2010-11-20 10:37:14 +00:00
/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
/* If you are missing that file, acquire a complete release at teeworlds.com. */
2023-07-17 13:49:41 +00:00
# include <base/log.h>
2010-05-29 07:25:38 +00:00
# include <base/math.h>
2020-09-26 19:41:58 +00:00
# include <base/system.h>
2023-02-17 17:07:17 +00:00
2023-07-17 13:49:41 +00:00
# include <engine/console.h>
2010-05-29 07:25:38 +00:00
# include <engine/graphics.h>
2023-07-17 13:49:41 +00:00
# include <engine/shared/json.h>
2018-03-13 20:49:07 +00:00
# include <engine/storage.h>
2020-09-26 19:41:58 +00:00
# include <engine/textrender.h>
2010-05-29 07:25:38 +00:00
// ft2 texture
# include <ft2build.h>
# include FT_FREETYPE_H
2023-02-17 17:07:17 +00:00
# include <chrono>
# include <cstddef>
2021-09-12 17:40:23 +00:00
# include <limits>
2023-07-17 13:49:41 +00:00
# include <tuple>
# include <unordered_map>
2020-09-26 19:41:58 +00:00
# include <vector>
2010-05-29 07:25:38 +00:00
2022-05-18 16:00:05 +00:00
using namespace std : : chrono_literals ;
2023-07-17 13:49:41 +00:00
enum
2010-05-29 07:25:38 +00:00
{
2023-07-17 13:49:41 +00:00
FONT_NAME_SIZE = 128 ,
} ;
struct SGlyph
{
enum class EState
{
UNINITIALIZED ,
RENDERED ,
ERROR ,
} ;
EState m_State = EState : : UNINITIALIZED ;
int m_FontSize ;
FT_Face m_Face ;
int m_Chr ;
FT_UInt m_GlyphIndex ;
2011-04-13 18:37:12 +00:00
2023-07-17 13:49:41 +00:00
// these values are scaled to the font size
2010-05-29 07:25:38 +00:00
// width * font_size == real_size
float m_Width ;
float m_Height ;
2021-09-12 17:40:23 +00:00
float m_CharWidth ;
float m_CharHeight ;
2010-05-29 07:25:38 +00:00
float m_OffsetX ;
float m_OffsetY ;
float m_AdvanceX ;
2011-04-13 18:37:12 +00:00
2018-03-13 20:49:07 +00:00
float m_aUVs [ 4 ] ;
2010-05-29 07:25:38 +00:00
} ;
2023-07-17 13:49:41 +00:00
struct SGlyphKeyHash
2018-03-13 20:49:07 +00:00
{
2023-07-17 13:49:41 +00:00
size_t operator ( ) ( const std : : tuple < FT_Face , int , int > & Key ) const
2018-03-13 20:49:07 +00:00
{
2023-07-17 13:49:41 +00:00
size_t Hash = 17 ;
Hash = Hash * 31 + std : : hash < FT_Face > ( ) ( std : : get < 0 > ( Key ) ) ;
Hash = Hash * 31 + std : : hash < int > ( ) ( std : : get < 1 > ( Key ) ) ;
Hash = Hash * 31 + std : : hash < int > ( ) ( std : : get < 2 > ( Key ) ) ;
return Hash ;
2018-03-13 20:49:07 +00:00
}
} ;
2011-04-13 18:37:12 +00:00
2023-07-17 13:49:41 +00:00
struct SGlyphKeyEquals
2018-03-13 20:49:07 +00:00
{
2023-07-17 13:49:41 +00:00
bool operator ( ) ( const std : : tuple < FT_Face , int , int > & Lhs , const std : : tuple < FT_Face , int , int > & Rhs ) const
{
return std : : get < 0 > ( Lhs ) = = std : : get < 0 > ( Rhs ) & & std : : get < 1 > ( Lhs ) = = std : : get < 1 > ( Rhs ) & & std : : get < 2 > ( Lhs ) = = std : : get < 2 > ( Rhs ) ;
}
2018-03-13 20:49:07 +00:00
} ;
2011-04-13 18:37:12 +00:00
2023-07-30 21:43:10 +00:00
class CAtlas
2018-03-13 20:49:07 +00:00
{
2023-07-30 21:43:10 +00:00
struct SSectionKeyHash
{
size_t operator ( ) ( const std : : tuple < size_t , size_t > & Key ) const
{
// Width and height should never be above 2^16 so this hash should cause no collisions
return ( std : : get < 0 > ( Key ) < < 16 ) ^ std : : get < 1 > ( Key ) ;
}
} ;
struct SSectionKeyEquals
{
bool operator ( ) ( const std : : tuple < size_t , size_t > & Lhs , const std : : tuple < size_t , size_t > & Rhs ) const
{
return std : : get < 0 > ( Lhs ) = = std : : get < 0 > ( Rhs ) & & std : : get < 1 > ( Lhs ) = = std : : get < 1 > ( Rhs ) ;
}
} ;
struct SSection
{
size_t m_X ;
size_t m_Y ;
size_t m_W ;
size_t m_H ;
SSection ( ) = default ;
SSection ( size_t X , size_t Y , size_t W , size_t H ) :
m_X ( X ) , m_Y ( Y ) , m_W ( W ) , m_H ( H )
{
}
} ;
/**
* Sections with a smaller width or height will not be created
* when cutting larger sections , to prevent collecting many
2023-09-05 19:24:33 +00:00
* small , mostly unusable sections .
2023-07-30 21:43:10 +00:00
*/
static constexpr size_t MIN_SECTION_DIMENSION = 6 ;
/**
* Sections with larger width or height will be stored in m_vSections .
* Sections with width and height equal or smaller will be stored in m_SectionsMap .
* This achieves a good balance between the size of the vector storing all large
* sections and the map storing vectors of all sections with specific small sizes .
* Lowering this value will result in the size of m_vSections becoming the bottleneck .
* Increasing this value will result in the map becoming the bottleneck .
*/
static constexpr size_t MAX_SECTION_DIMENSION_MAPPED = 8 * MIN_SECTION_DIMENSION ;
size_t m_TextureDimension ;
std : : vector < SSection > m_vSections ;
std : : unordered_map < std : : tuple < size_t , size_t > , std : : vector < SSection > , SSectionKeyHash , SSectionKeyEquals > m_SectionsMap ;
void AddSection ( size_t X , size_t Y , size_t W , size_t H )
{
std : : vector < SSection > & vSections = W < = MAX_SECTION_DIMENSION_MAPPED & & H < = MAX_SECTION_DIMENSION_MAPPED ? m_SectionsMap [ std : : make_tuple ( W , H ) ] : m_vSections ;
vSections . emplace_back ( X , Y , W , H ) ;
}
void UseSection ( const SSection & Section , size_t Width , size_t Height , int & PosX , int & PosY )
{
PosX = Section . m_X ;
PosY = Section . m_Y ;
// Create cut sections
const size_t CutW = Section . m_W - Width ;
const size_t CutH = Section . m_H - Height ;
if ( CutW = = 0 )
{
if ( CutH > = MIN_SECTION_DIMENSION )
AddSection ( Section . m_X , Section . m_Y + Height , Section . m_W , CutH ) ;
}
else if ( CutH = = 0 )
{
if ( CutW > = MIN_SECTION_DIMENSION )
AddSection ( Section . m_X + Width , Section . m_Y , CutW , Section . m_H ) ;
}
else if ( CutW > CutH )
{
if ( CutW > = MIN_SECTION_DIMENSION )
AddSection ( Section . m_X + Width , Section . m_Y , CutW , Section . m_H ) ;
if ( CutH > = MIN_SECTION_DIMENSION )
AddSection ( Section . m_X , Section . m_Y + Height , Width , CutH ) ;
}
else
{
if ( CutH > = MIN_SECTION_DIMENSION )
AddSection ( Section . m_X , Section . m_Y + Height , Section . m_W , CutH ) ;
if ( CutW > = MIN_SECTION_DIMENSION )
AddSection ( Section . m_X + Width , Section . m_Y , CutW , Height ) ;
}
}
public :
void Clear ( size_t TextureDimension )
{
m_TextureDimension = TextureDimension ;
m_vSections . clear ( ) ;
m_vSections . emplace_back ( 0 , 0 , m_TextureDimension , m_TextureDimension ) ;
m_SectionsMap . clear ( ) ;
}
void IncreaseDimension ( size_t NewTextureDimension )
{
dbg_assert ( NewTextureDimension = = m_TextureDimension * 2 , " New atlas dimension must be twice the old one " ) ;
// Create 3 square sections to cover the new area, add the sections
// to the beginning of the vector so they are considered last.
m_vSections . emplace_back ( m_TextureDimension , m_TextureDimension , m_TextureDimension , m_TextureDimension ) ;
m_vSections . emplace_back ( m_TextureDimension , 0 , m_TextureDimension , m_TextureDimension ) ;
m_vSections . emplace_back ( 0 , m_TextureDimension , m_TextureDimension , m_TextureDimension ) ;
std : : rotate ( m_vSections . rbegin ( ) , m_vSections . rbegin ( ) + 3 , m_vSections . rend ( ) ) ;
m_TextureDimension = NewTextureDimension ;
}
bool Add ( size_t Width , size_t Height , int & PosX , int & PosY )
{
if ( m_vSections . empty ( ) | | m_TextureDimension < Width | | m_TextureDimension < Height )
return false ;
// Find small section more efficiently by using maps
if ( Width < = MAX_SECTION_DIMENSION_MAPPED & & Height < = MAX_SECTION_DIMENSION_MAPPED )
{
const auto UseSectionFromVector = [ & ] ( std : : vector < SSection > & vSections ) {
if ( ! vSections . empty ( ) )
{
const SSection Section = vSections . back ( ) ;
vSections . pop_back ( ) ;
UseSection ( Section , Width , Height , PosX , PosY ) ;
return true ;
}
return false ;
} ;
if ( UseSectionFromVector ( m_SectionsMap [ std : : make_tuple ( Width , Height ) ] ) )
return true ;
for ( size_t CheckWidth = Width + 1 ; CheckWidth < = MAX_SECTION_DIMENSION_MAPPED ; + + CheckWidth )
{
if ( UseSectionFromVector ( m_SectionsMap [ std : : make_tuple ( CheckWidth , Height ) ] ) )
return true ;
}
for ( size_t CheckHeight = Height + 1 ; CheckHeight < = MAX_SECTION_DIMENSION_MAPPED ; + + CheckHeight )
{
if ( UseSectionFromVector ( m_SectionsMap [ std : : make_tuple ( Width , CheckHeight ) ] ) )
return true ;
}
// We don't iterate sections in the map with increasing width and height at the same time,
2023-09-05 19:24:33 +00:00
// because it's slower and doesn't noticeable increase the atlas utilization.
2023-07-30 21:43:10 +00:00
}
// Check vector for larger section
size_t SmallestLossValue = std : : numeric_limits < size_t > : : max ( ) ;
size_t SmallestLossIndex = m_vSections . size ( ) ;
size_t SectionIndex = m_vSections . size ( ) ;
do
{
- - SectionIndex ;
const SSection & Section = m_vSections [ SectionIndex ] ;
if ( Section . m_W < Width | | Section . m_H < Height )
continue ;
const size_t LossW = Section . m_W - Width ;
const size_t LossH = Section . m_H - Height ;
size_t Loss ;
if ( LossW = = 0 )
Loss = LossH ;
else if ( LossH = = 0 )
Loss = LossW ;
else
Loss = LossW * LossH ;
if ( Loss < SmallestLossValue )
{
SmallestLossValue = Loss ;
SmallestLossIndex = SectionIndex ;
if ( SmallestLossValue = = 0 )
break ;
}
} while ( SectionIndex > 0 ) ;
if ( SmallestLossIndex = = m_vSections . size ( ) )
2023-09-05 19:24:33 +00:00
return false ; // No usable section found in vector
2023-07-30 21:43:10 +00:00
// Use the section with the smallest loss
const SSection Section = m_vSections [ SmallestLossIndex ] ;
m_vSections . erase ( m_vSections . begin ( ) + SmallestLossIndex ) ;
UseSection ( Section , Width , Height , PosX , PosY ) ;
return true ;
}
2010-05-29 07:25:38 +00:00
} ;
2023-07-17 13:49:41 +00:00
class CGlyphMap
2010-05-29 07:25:38 +00:00
{
2010-07-05 18:57:07 +00:00
public :
2023-07-17 13:49:41 +00:00
enum
2020-08-20 09:22:25 +00:00
{
2023-07-17 13:49:41 +00:00
FONT_TEXTURE_FILL = 0 , // the main text body
FONT_TEXTURE_OUTLINE , // the text outline
NUM_FONT_TEXTURES ,
2020-08-20 09:22:25 +00:00
} ;
2023-07-17 13:49:41 +00:00
private :
/**
* The initial dimension of the atlas textures .
* Results in 1 MB of memory being used per texture .
*/
static constexpr int INITIAL_ATLAS_DIMENSION = 1024 ;
/**
* The maximum dimension of the atlas textures .
* Results in 256 MB of memory being used per texture .
*/
static constexpr int MAXIMUM_ATLAS_DIMENSION = 16 * 1024 ;
/**
* The minimum supported font size .
*/
static constexpr int MIN_FONT_SIZE = 6 ;
/**
* The maximum supported font size .
*/
static constexpr int MAX_FONT_SIZE = 128 ;
/**
* White square to indicate missing glyph .
*/
static constexpr int REPLACEMENT_CHARACTER = 0x25a1 ;
2010-05-29 07:25:38 +00:00
IGraphics * m_pGraphics ;
IGraphics * Graphics ( ) { return m_pGraphics ; }
2011-04-13 18:37:12 +00:00
2023-07-17 13:49:41 +00:00
// Atlas textures and data
IGraphics : : CTextureHandle m_aTextures [ NUM_FONT_TEXTURES ] ;
// Width and height are the same, all font textures have the same dimensions
size_t m_TextureDimension = INITIAL_ATLAS_DIMENSION ;
// Keep the full texture data, because OpenGL doesn't provide texture copying
uint8_t * m_apTextureData [ NUM_FONT_TEXTURES ] ;
2023-07-30 21:43:10 +00:00
CAtlas m_TextureAtlas ;
2023-07-17 13:49:41 +00:00
std : : unordered_map < std : : tuple < FT_Face , int , int > , SGlyph , SGlyphKeyHash , SGlyphKeyEquals > m_Glyphs ;
// Data used for rendering glyphs
uint8_t m_aaGlyphData [ NUM_FONT_TEXTURES ] [ 64 * 1024 ] ;
// Font faces
2023-08-07 15:50:31 +00:00
FT_Face m_DefaultFace = nullptr ;
FT_Face m_IconFace = nullptr ;
FT_Face m_VariantFace = nullptr ;
FT_Face m_SelectedFace = nullptr ;
2023-07-17 13:49:41 +00:00
std : : vector < FT_Face > m_vFallbackFaces ;
std : : vector < FT_Face > m_vFtFaces ;
FT_Face GetFaceByName ( const char * pFamilyName )
{
if ( pFamilyName = = nullptr | | pFamilyName [ 0 ] = = ' \0 ' )
return nullptr ;
2018-03-13 20:49:07 +00:00
2023-07-17 13:49:41 +00:00
FT_Face FamilyNameMatch = nullptr ;
char aFamilyStyleName [ FONT_NAME_SIZE ] ;
2018-03-13 20:49:07 +00:00
2023-07-17 13:49:41 +00:00
for ( const auto & CurrentFace : m_vFtFaces )
{
// Best match: font face with matching family and style name
str_format ( aFamilyStyleName , sizeof ( aFamilyStyleName ) , " %s %s " , CurrentFace - > family_name , CurrentFace - > style_name ) ;
if ( str_comp ( pFamilyName , aFamilyStyleName ) = = 0 )
{
return CurrentFace ;
}
2019-04-26 19:36:49 +00:00
2023-07-17 13:49:41 +00:00
// Second best match: font face with matching family
if ( ! FamilyNameMatch & & str_comp ( pFamilyName , CurrentFace - > family_name ) = = 0 )
{
FamilyNameMatch = CurrentFace ;
}
}
2018-03-13 20:49:07 +00:00
2023-07-17 13:49:41 +00:00
return FamilyNameMatch ;
}
2021-09-16 14:50:17 +00:00
2023-07-17 13:49:41 +00:00
bool IncreaseGlyphMapSize ( )
2018-03-13 20:49:07 +00:00
{
2023-07-17 13:49:41 +00:00
if ( m_TextureDimension > = MAXIMUM_ATLAS_DIMENSION )
return false ;
const size_t NewTextureDimension = m_TextureDimension * 2 ;
log_debug ( " textrender " , " Increasing atlas dimension to % " PRIzu " (% " PRIzu " MB used for textures) " , NewTextureDimension , ( NewTextureDimension / 1024 ) * ( NewTextureDimension / 1024 ) * NUM_FONT_TEXTURES ) ;
UnloadTextures ( ) ;
for ( auto & pTextureData : m_apTextureData )
2018-03-13 20:49:07 +00:00
{
2023-07-17 13:49:41 +00:00
uint8_t * pTmpTexBuffer = new uint8_t [ NewTextureDimension * NewTextureDimension ] ;
mem_zero ( pTmpTexBuffer , NewTextureDimension * NewTextureDimension * sizeof ( uint8_t ) ) ;
for ( size_t y = 0 ; y < m_TextureDimension ; + + y )
{
mem_copy ( & pTmpTexBuffer [ y * NewTextureDimension ] , & pTextureData [ y * m_TextureDimension ] , m_TextureDimension ) ;
}
delete [ ] pTextureData ;
pTextureData = pTmpTexBuffer ;
2018-03-13 20:49:07 +00:00
}
2023-07-30 21:43:10 +00:00
m_TextureAtlas . IncreaseDimension ( NewTextureDimension ) ;
2023-07-17 13:49:41 +00:00
m_TextureDimension = NewTextureDimension ;
UploadTextures ( ) ;
return true ;
2018-03-13 20:49:07 +00:00
}
2023-07-17 13:49:41 +00:00
void UploadTextures ( )
2018-03-13 20:49:07 +00:00
{
2023-07-17 13:49:41 +00:00
const size_t NewTextureSize = m_TextureDimension * m_TextureDimension ;
2024-03-09 10:11:50 +00:00
uint8_t * pTmpTextFillData = static_cast < uint8_t * > ( malloc ( NewTextureSize ) ) ;
uint8_t * pTmpTextOutlineData = static_cast < uint8_t * > ( malloc ( NewTextureSize ) ) ;
2023-07-17 13:49:41 +00:00
mem_copy ( pTmpTextFillData , m_apTextureData [ FONT_TEXTURE_FILL ] , NewTextureSize ) ;
mem_copy ( pTmpTextOutlineData , m_apTextureData [ FONT_TEXTURE_OUTLINE ] , NewTextureSize ) ;
Graphics ( ) - > LoadTextTextures ( m_TextureDimension , m_TextureDimension , m_aTextures [ FONT_TEXTURE_FILL ] , m_aTextures [ FONT_TEXTURE_OUTLINE ] , pTmpTextFillData , pTmpTextOutlineData ) ;
2018-03-13 20:49:07 +00:00
}
2023-07-17 13:49:41 +00:00
void UnloadTextures ( )
2020-10-13 20:08:52 +00:00
{
2023-07-17 13:49:41 +00:00
Graphics ( ) - > UnloadTextTextures ( m_aTextures [ FONT_TEXTURE_FILL ] , m_aTextures [ FONT_TEXTURE_OUTLINE ] ) ;
2020-10-13 20:08:52 +00:00
}
2023-07-17 13:49:41 +00:00
FT_UInt GetCharGlyph ( int Chr , FT_Face * pFace , bool AllowReplacementCharacter )
2018-03-13 20:49:07 +00:00
{
2023-07-17 13:49:41 +00:00
for ( FT_Face Face : { m_SelectedFace , m_DefaultFace , m_VariantFace } )
2018-03-13 20:49:07 +00:00
{
2023-07-17 13:49:41 +00:00
if ( Face & & Face - > charmap )
{
FT_UInt GlyphIndex = FT_Get_Char_Index ( Face , ( FT_ULong ) Chr ) ;
if ( GlyphIndex )
{
* pFace = Face ;
return GlyphIndex ;
}
}
2018-03-13 20:49:07 +00:00
}
2023-07-17 13:49:41 +00:00
for ( const auto & FallbackFace : m_vFallbackFaces )
2023-05-14 17:49:24 +00:00
{
2023-07-17 13:49:41 +00:00
if ( FallbackFace - > charmap )
{
FT_UInt GlyphIndex = FT_Get_Char_Index ( FallbackFace , ( FT_ULong ) Chr ) ;
if ( GlyphIndex )
{
* pFace = FallbackFace ;
return GlyphIndex ;
}
}
2023-05-14 17:49:24 +00:00
}
2018-03-13 20:49:07 +00:00
2023-07-17 13:49:41 +00:00
if ( ! m_DefaultFace | | ! m_DefaultFace - > charmap | | ! AllowReplacementCharacter )
2010-05-29 07:25:38 +00:00
{
2023-07-17 13:49:41 +00:00
* pFace = nullptr ;
return 0 ;
2010-05-29 07:25:38 +00:00
}
2023-07-17 13:49:41 +00:00
FT_UInt GlyphIndex = FT_Get_Char_Index ( m_DefaultFace , ( FT_ULong ) REPLACEMENT_CHARACTER ) ;
* pFace = m_DefaultFace ;
2019-04-26 19:36:49 +00:00
2023-07-17 13:49:41 +00:00
if ( GlyphIndex = = 0 )
{
log_debug ( " textrender " , " Default font has no glyph for either %d or replacement char %d. " , Chr , REPLACEMENT_CHARACTER ) ;
}
2019-04-26 19:36:49 +00:00
2023-07-17 13:49:41 +00:00
return GlyphIndex ;
2020-10-22 20:21:19 +00:00
}
2023-02-17 17:07:17 +00:00
void Grow ( const unsigned char * pIn , unsigned char * pOut , int w , int h , int OutlineCount ) const
2010-05-29 07:25:38 +00:00
{
2011-04-13 18:37:12 +00:00
for ( int y = 0 ; y < h ; y + + )
2023-02-17 17:07:17 +00:00
{
2011-04-13 18:37:12 +00:00
for ( int x = 0 ; x < w ; x + + )
{
2020-09-26 19:41:58 +00:00
int c = pIn [ y * w + x ] ;
2010-05-29 07:25:38 +00:00
2020-09-14 14:42:33 +00:00
for ( int sy = - OutlineCount ; sy < = OutlineCount ; sy + + )
2023-02-17 17:07:17 +00:00
{
2020-09-14 14:42:33 +00:00
for ( int sx = - OutlineCount ; sx < = OutlineCount ; sx + + )
2010-05-29 07:25:38 +00:00
{
2020-09-26 19:41:58 +00:00
int GetX = x + sx ;
int GetY = y + sy ;
2018-03-13 20:49:07 +00:00
if ( GetX > = 0 & & GetY > = 0 & & GetX < w & & GetY < h )
2010-05-29 07:25:38 +00:00
{
2020-09-26 19:41:58 +00:00
int Index = GetY * w + GetX ;
2024-01-08 20:52:14 +00:00
float Mask = 1.f - clamp ( length ( vec2 ( sx , sy ) ) - OutlineCount , 0.f , 1.f ) ;
c = maximum ( c , int ( pIn [ Index ] * Mask ) ) ;
2010-05-29 07:25:38 +00:00
}
}
2023-02-17 17:07:17 +00:00
}
2010-05-29 07:25:38 +00:00
2020-09-26 19:41:58 +00:00
pOut [ y * w + x ] = c ;
2010-05-29 07:25:38 +00:00
}
2023-02-17 17:07:17 +00:00
}
2010-05-29 07:25:38 +00:00
}
2023-02-17 17:07:17 +00:00
int AdjustOutlineThicknessToFontSize ( int OutlineThickness , int FontSize ) const
2011-01-29 17:48:55 +00:00
{
2018-03-21 14:43:56 +00:00
if ( FontSize > 48 )
2011-01-29 17:48:55 +00:00
OutlineThickness * = 4 ;
else if ( FontSize > = 18 )
OutlineThickness * = 2 ;
return OutlineThickness ;
2011-01-04 11:30:40 +00:00
}
2023-07-17 13:49:41 +00:00
void UploadGlyph ( int TextureIndex , int PosX , int PosY , size_t Width , size_t Height , const unsigned char * pData )
2010-05-29 07:25:38 +00:00
{
2023-07-17 13:49:41 +00:00
for ( size_t y = 0 ; y < Height ; + + y )
2018-03-13 20:49:07 +00:00
{
2023-07-17 13:49:41 +00:00
mem_copy ( & m_apTextureData [ TextureIndex ] [ PosX + ( ( y + PosY ) * m_TextureDimension ) ] , & pData [ y * Width ] , Width ) ;
2018-03-13 20:49:07 +00:00
}
2023-07-17 13:49:41 +00:00
Graphics ( ) - > UpdateTextTexture ( m_aTextures [ TextureIndex ] , PosX , PosY , Width , Height , pData ) ;
2010-05-29 07:25:38 +00:00
}
2023-07-30 21:43:10 +00:00
bool FitGlyph ( size_t Width , size_t Height , int & PosX , int & PosY )
2010-05-29 07:25:38 +00:00
{
2023-07-30 21:43:10 +00:00
return m_TextureAtlas . Add ( Width , Height , PosX , PosY ) ;
2010-05-29 07:25:38 +00:00
}
2023-07-17 13:49:41 +00:00
bool RenderGlyph ( SGlyph & Glyph )
2023-02-17 17:07:17 +00:00
{
2023-07-17 13:49:41 +00:00
FT_Set_Pixel_Sizes ( Glyph . m_Face , 0 , Glyph . m_FontSize ) ;
2020-08-20 06:54:59 +00:00
2023-07-17 13:49:41 +00:00
if ( FT_Load_Glyph ( Glyph . m_Face , Glyph . m_GlyphIndex , FT_LOAD_RENDER | FT_LOAD_NO_BITMAP ) )
{
log_debug ( " textrender " , " Error loading glyph. Chr=%d GlyphIndex=%u " , Glyph . m_Chr , Glyph . m_GlyphIndex ) ;
return false ;
}
2010-05-29 07:25:38 +00:00
2023-07-17 13:49:41 +00:00
const FT_Bitmap * pBitmap = & Glyph . m_Face - > glyph - > bitmap ;
2019-01-06 05:42:57 +00:00
2023-07-17 13:49:41 +00:00
const unsigned RealWidth = pBitmap - > width ;
const unsigned RealHeight = pBitmap - > rows ;
// adjust spacing
int OutlineThickness = 0 ;
int x = 0 ;
int y = 0 ;
if ( RealWidth > 0 )
2019-03-28 21:38:20 +00:00
{
2023-07-17 13:49:41 +00:00
OutlineThickness = AdjustOutlineThicknessToFontSize ( 1 , Glyph . m_FontSize ) ;
x + = ( OutlineThickness + 1 ) ;
y + = ( OutlineThickness + 1 ) ;
}
const unsigned Width = RealWidth + x * 2 ;
const unsigned Height = RealHeight + y * 2 ;
int X = 0 ;
int Y = 0 ;
2020-08-20 09:22:25 +00:00
2023-07-17 13:49:41 +00:00
if ( Width > 0 & & Height > 0 )
{
// find space in atlas, or increase size if necessary
2023-07-30 21:43:10 +00:00
while ( ! FitGlyph ( Width , Height , X , Y ) )
2023-07-17 13:49:41 +00:00
{
if ( ! IncreaseGlyphMapSize ( ) )
{
log_debug ( " textrender " , " Cannot fit glyph into atlas, which is already at maximum size. Chr=%d GlyphIndex=%u " , Glyph . m_Chr , Glyph . m_GlyphIndex ) ;
return false ;
}
}
2020-08-20 06:54:59 +00:00
2023-07-17 13:49:41 +00:00
// prepare glyph data
mem_zero ( m_aaGlyphData [ FONT_TEXTURE_FILL ] , ( size_t ) Width * Height * sizeof ( uint8_t ) ) ;
for ( unsigned py = 0 ; py < pBitmap - > rows ; + + py )
{
mem_copy ( & m_aaGlyphData [ FONT_TEXTURE_FILL ] [ ( py + y ) * Width + x ] , & pBitmap - > buffer [ py * pBitmap - > width ] , pBitmap - > width ) ;
2020-08-20 06:54:59 +00:00
}
2019-03-28 21:38:20 +00:00
2023-07-17 13:49:41 +00:00
// upload the glyph
UploadGlyph ( FONT_TEXTURE_FILL , X , Y , Width , Height , m_aaGlyphData [ FONT_TEXTURE_FILL ] ) ;
Grow ( m_aaGlyphData [ FONT_TEXTURE_FILL ] , m_aaGlyphData [ FONT_TEXTURE_OUTLINE ] , Width , Height , OutlineThickness ) ;
UploadGlyph ( FONT_TEXTURE_OUTLINE , X , Y , Width , Height , m_aaGlyphData [ FONT_TEXTURE_OUTLINE ] ) ;
}
// set glyph info
{
const int BmpWidth = pBitmap - > width + x * 2 ;
const int BmpHeight = pBitmap - > rows + y * 2 ;
Glyph . m_Height = Height ;
Glyph . m_Width = Width ;
Glyph . m_CharHeight = RealHeight ;
Glyph . m_CharWidth = RealWidth ;
Glyph . m_OffsetX = ( Glyph . m_Face - > glyph - > metrics . horiBearingX > > 6 ) ;
Glyph . m_OffsetY = - ( ( Glyph . m_Face - > glyph - > metrics . height > > 6 ) - ( Glyph . m_Face - > glyph - > metrics . horiBearingY > > 6 ) ) ;
Glyph . m_AdvanceX = ( Glyph . m_Face - > glyph - > advance . x > > 6 ) ;
Glyph . m_aUVs [ 0 ] = X ;
Glyph . m_aUVs [ 1 ] = Y ;
Glyph . m_aUVs [ 2 ] = Glyph . m_aUVs [ 0 ] + BmpWidth ;
Glyph . m_aUVs [ 3 ] = Glyph . m_aUVs [ 1 ] + BmpHeight ;
Glyph . m_State = SGlyph : : EState : : RENDERED ;
}
return true ;
}
public :
CGlyphMap ( IGraphics * pGraphics )
{
m_pGraphics = pGraphics ;
for ( auto & pTextureData : m_apTextureData )
{
pTextureData = new uint8_t [ m_TextureDimension * m_TextureDimension ] ;
mem_zero ( pTextureData , m_TextureDimension * m_TextureDimension * sizeof ( uint8_t ) ) ;
}
2023-07-30 21:43:10 +00:00
m_TextureAtlas . Clear ( m_TextureDimension ) ;
2023-07-17 13:49:41 +00:00
UploadTextures ( ) ;
}
~ CGlyphMap ( )
{
UnloadTextures ( ) ;
for ( auto & pTextureData : m_apTextureData )
{
delete [ ] pTextureData ;
}
}
FT_Face DefaultFace ( ) const
{
return m_DefaultFace ;
}
FT_Face IconFace ( ) const
{
return m_IconFace ;
}
void AddFace ( FT_Face Face )
{
m_vFtFaces . push_back ( Face ) ;
if ( ! m_DefaultFace )
m_DefaultFace = Face ;
}
void SetDefaultFaceByName ( const char * pFamilyName )
{
m_DefaultFace = GetFaceByName ( pFamilyName ) ;
}
void SetIconFaceByName ( const char * pFamilyName )
{
m_IconFace = GetFaceByName ( pFamilyName ) ;
}
void AddFallbackFaceByName ( const char * pFamilyName )
{
FT_Face Face = GetFaceByName ( pFamilyName ) ;
if ( Face ! = nullptr & & std : : find ( m_vFallbackFaces . begin ( ) , m_vFallbackFaces . end ( ) , Face ) = = m_vFallbackFaces . end ( ) )
{
m_vFallbackFaces . push_back ( Face ) ;
}
}
void SetVariantFaceByName ( const char * pFamilyName )
{
FT_Face Face = GetFaceByName ( pFamilyName ) ;
if ( m_VariantFace ! = Face )
{
m_VariantFace = Face ;
Clear ( ) ; // rebuild atlas after changing variant font
}
}
void SetFontPreset ( EFontPreset FontPreset )
{
switch ( FontPreset )
{
case EFontPreset : : DEFAULT_FONT :
m_SelectedFace = nullptr ;
break ;
case EFontPreset : : ICON_FONT :
m_SelectedFace = m_IconFace ;
break ;
}
}
void Clear ( )
{
for ( size_t TextureIndex = 0 ; TextureIndex < NUM_FONT_TEXTURES ; + + TextureIndex )
{
mem_zero ( m_apTextureData [ TextureIndex ] , m_TextureDimension * m_TextureDimension * sizeof ( uint8_t ) ) ;
Graphics ( ) - > UpdateTextTexture ( m_aTextures [ TextureIndex ] , 0 , 0 , m_TextureDimension , m_TextureDimension , m_apTextureData [ TextureIndex ] ) ;
}
2023-07-30 21:43:10 +00:00
m_TextureAtlas . Clear ( m_TextureDimension ) ;
2023-07-17 13:49:41 +00:00
m_Glyphs . clear ( ) ;
}
const SGlyph * GetGlyph ( int Chr , int FontSize )
{
FontSize = clamp ( FontSize , MIN_FONT_SIZE , MAX_FONT_SIZE ) ;
// Find glyph index and most appropriate font face.
FT_Face Face ;
FT_UInt GlyphIndex = GetCharGlyph ( Chr , & Face , false ) ;
if ( GlyphIndex = = 0 )
{
// Use replacement character if glyph could not be found,
// also retrieve replacement character from the atlas.
return Chr = = REPLACEMENT_CHARACTER ? nullptr : GetGlyph ( REPLACEMENT_CHARACTER , FontSize ) ;
}
// Check if glyph for this (font face, character, font size)-combination was already rendered.
SGlyph & Glyph = m_Glyphs [ std : : make_tuple ( Face , Chr , FontSize ) ] ;
if ( Glyph . m_State = = SGlyph : : EState : : RENDERED )
return & Glyph ;
else if ( Glyph . m_State = = SGlyph : : EState : : ERROR )
return nullptr ;
// Else, render it.
Glyph . m_FontSize = FontSize ;
Glyph . m_Face = Face ;
Glyph . m_Chr = Chr ;
Glyph . m_GlyphIndex = GlyphIndex ;
if ( RenderGlyph ( Glyph ) )
return & Glyph ;
// Use replacement character if the glyph could not be rendered,
// also retrieve replacement character from the atlas.
const SGlyph * pReplacementCharacter = Chr = = REPLACEMENT_CHARACTER ? nullptr : GetGlyph ( REPLACEMENT_CHARACTER , FontSize ) ;
if ( pReplacementCharacter )
{
Glyph = * pReplacementCharacter ;
return & Glyph ;
}
// Keep failed glyph in the cache so we don't attempt to render it again,
// but set its state to ERROR so we don't return it to the text render.
Glyph . m_State = SGlyph : : EState : : ERROR ;
return nullptr ;
}
vec2 Kerning ( const SGlyph * pLeft , const SGlyph * pRight ) const
{
if ( pLeft ! = nullptr & & pRight ! = nullptr & & pLeft - > m_Face = = pRight - > m_Face & & pLeft - > m_FontSize = = pRight - > m_FontSize )
{
FT_Vector Kerning = { 0 , 0 } ;
FT_Set_Pixel_Sizes ( pLeft - > m_Face , 0 , pLeft - > m_FontSize ) ;
FT_Get_Kerning ( pLeft - > m_Face , pLeft - > m_Chr , pRight - > m_Chr , FT_KERNING_DEFAULT , & Kerning ) ;
return vec2 ( Kerning . x > > 6 , Kerning . y > > 6 ) ;
}
return vec2 ( 0.0f , 0.0f ) ;
}
2024-03-24 12:27:31 +00:00
void UploadEntityLayerText ( const CImageInfo & TextImage , int TexSubWidth , int TexSubHeight , const char * pText , int Length , float x , float y , int FontSize )
2023-07-17 13:49:41 +00:00
{
if ( FontSize < 1 )
return ;
2024-03-24 12:27:31 +00:00
const size_t PixelSize = TextImage . PixelSize ( ) ;
2023-07-17 13:49:41 +00:00
const char * pCurrent = pText ;
const char * pEnd = pCurrent + Length ;
int WidthLastChars = 0 ;
while ( pCurrent < pEnd )
{
const char * pTmp = pCurrent ;
const int NextCharacter = str_utf8_decode ( & pTmp ) ;
if ( NextCharacter )
{
FT_Face Face ;
FT_UInt GlyphIndex = GetCharGlyph ( NextCharacter , & Face , true ) ;
if ( GlyphIndex = = 0 )
{
pCurrent = pTmp ;
continue ;
}
FT_Set_Pixel_Sizes ( Face , 0 , FontSize ) ;
if ( FT_Load_Char ( Face , NextCharacter , FT_LOAD_RENDER | FT_LOAD_NO_BITMAP ) )
{
log_debug ( " textrender " , " Error loading glyph. Chr=%d GlyphIndex=%u " , NextCharacter , GlyphIndex ) ;
pCurrent = pTmp ;
continue ;
}
const FT_Bitmap * pBitmap = & Face - > glyph - > bitmap ;
// prepare glyph data
const size_t GlyphDataSize = ( size_t ) pBitmap - > width * pBitmap - > rows * sizeof ( uint8_t ) ;
if ( pBitmap - > pixel_mode = = FT_PIXEL_MODE_GRAY )
mem_copy ( m_aaGlyphData [ FONT_TEXTURE_FILL ] , pBitmap - > buffer , GlyphDataSize ) ;
else
mem_zero ( m_aaGlyphData [ FONT_TEXTURE_FILL ] , GlyphDataSize ) ;
for ( unsigned OffY = 0 ; OffY < pBitmap - > rows ; + + OffY )
{
for ( unsigned OffX = 0 ; OffX < pBitmap - > width ; + + OffX )
{
const int ImgOffX = clamp ( x + OffX + WidthLastChars , x , ( x + TexSubWidth ) - 1 ) ;
const int ImgOffY = clamp ( y + OffY , y , ( y + TexSubHeight ) - 1 ) ;
2024-03-24 12:27:31 +00:00
const size_t ImageOffset = ImgOffY * ( TextImage . m_Width * PixelSize ) + ImgOffX * PixelSize ;
2023-07-17 13:49:41 +00:00
const size_t GlyphOffset = OffY * pBitmap - > width + OffX ;
2023-04-27 15:15:48 +00:00
for ( size_t i = 0 ; i < PixelSize ; + + i )
2023-07-17 13:49:41 +00:00
{
2023-04-27 15:15:48 +00:00
if ( i ! = PixelSize - 1 )
2023-07-17 13:49:41 +00:00
{
2024-03-24 12:27:31 +00:00
* ( TextImage . m_pData + ImageOffset + i ) = 255 ;
2023-07-17 13:49:41 +00:00
}
else
{
2024-03-24 12:27:31 +00:00
* ( TextImage . m_pData + ImageOffset + i ) = * ( m_aaGlyphData [ FONT_TEXTURE_FILL ] + GlyphOffset ) ;
2023-07-17 13:49:41 +00:00
}
}
}
}
WidthLastChars + = ( pBitmap - > width + 1 ) ;
}
pCurrent = pTmp ;
}
}
size_t TextureDimension ( ) const
{
return m_TextureDimension ;
}
IGraphics : : CTextureHandle Texture ( size_t TextureIndex ) const
{
return m_aTextures [ TextureIndex ] ;
}
} ;
typedef vector4_base < unsigned char > STextCharQuadVertexColor ;
struct STextCharQuadVertex
{
STextCharQuadVertex ( )
{
m_Color . r = m_Color . g = m_Color . b = m_Color . a = 255 ;
}
float m_X , m_Y ;
// do not use normalized floats as coordinates, since the texture might grow
float m_U , m_V ;
STextCharQuadVertexColor m_Color ;
} ;
struct STextCharQuad
{
STextCharQuadVertex m_aVertices [ 4 ] ;
} ;
struct SStringInfo
{
int m_QuadBufferObjectIndex ;
int m_QuadBufferContainerIndex ;
int m_SelectionQuadContainerIndex ;
std : : vector < STextCharQuad > m_vCharacterQuads ;
} ;
struct STextContainer
{
STextContainer ( )
{
Reset ( ) ;
}
SStringInfo m_StringInfo ;
// keep these values to calculate offsets
float m_AlignedStartX ;
float m_AlignedStartY ;
float m_X ;
float m_Y ;
int m_Flags ;
int m_LineCount ;
int m_GlyphCount ;
int m_CharCount ;
int m_MaxLines ;
float m_LineWidth ;
unsigned m_RenderFlags ;
bool m_HasCursor ;
bool m_ForceCursorRendering ;
bool m_HasSelection ;
bool m_SingleTimeUse ;
STextBoundingBox m_BoundingBox ;
// prefix of the container's text stored for debugging purposes
char m_aDebugText [ 32 ] ;
STextContainerIndex m_ContainerIndex ;
void Reset ( )
{
m_StringInfo . m_QuadBufferObjectIndex = m_StringInfo . m_QuadBufferContainerIndex = m_StringInfo . m_SelectionQuadContainerIndex = - 1 ;
m_StringInfo . m_vCharacterQuads . clear ( ) ;
m_AlignedStartX = m_AlignedStartY = m_X = m_Y = 0.0f ;
m_Flags = m_LineCount = m_CharCount = m_GlyphCount = 0 ;
m_MaxLines = - 1 ;
m_LineWidth = - 1.0f ;
m_RenderFlags = 0 ;
m_HasCursor = false ;
m_ForceCursorRendering = false ;
m_HasSelection = false ;
m_SingleTimeUse = false ;
m_BoundingBox = { 0.0f , 0.0f , 0.0f , 0.0f } ;
m_aDebugText [ 0 ] = ' \0 ' ;
m_ContainerIndex = STextContainerIndex { } ;
}
} ;
struct SFontLanguageVariant
{
char m_aLanguageFile [ IO_MAX_PATH_LENGTH ] ;
char m_aFamilyName [ FONT_NAME_SIZE ] ;
} ;
class CTextRender : public IEngineTextRender
{
IConsole * m_pConsole ;
IGraphics * m_pGraphics ;
IStorage * m_pStorage ;
IConsole * Console ( ) { return m_pConsole ; }
IGraphics * Graphics ( ) { return m_pGraphics ; }
IStorage * Storage ( ) { return m_pStorage ; }
CGlyphMap * m_pGlyphMap ;
std : : vector < void * > m_vpFontData ;
std : : vector < SFontLanguageVariant > m_vVariants ;
unsigned m_RenderFlags ;
ColorRGBA m_Color ;
ColorRGBA m_OutlineColor ;
ColorRGBA m_SelectionColor ;
FT_Library m_FTLibrary ;
std : : vector < STextContainer * > m_vpTextContainers ;
std : : vector < int > m_vTextContainerIndices ;
int m_FirstFreeTextContainerIndex ;
SBufferContainerInfo m_DefaultTextContainerInfo ;
std : : chrono : : nanoseconds m_CursorRenderTime ;
int GetFreeTextContainerIndex ( )
{
if ( m_FirstFreeTextContainerIndex = = - 1 )
{
const int Index = ( int ) m_vTextContainerIndices . size ( ) ;
m_vTextContainerIndices . push_back ( Index ) ;
return Index ;
}
else
{
const int Index = m_FirstFreeTextContainerIndex ;
m_FirstFreeTextContainerIndex = m_vTextContainerIndices [ Index ] ;
m_vTextContainerIndices [ Index ] = Index ;
return Index ;
}
}
void FreeTextContainerIndex ( STextContainerIndex & Index )
{
m_vTextContainerIndices [ Index . m_Index ] = m_FirstFreeTextContainerIndex ;
m_FirstFreeTextContainerIndex = Index . m_Index ;
Index . Reset ( ) ;
}
void FreeTextContainer ( STextContainerIndex & Index )
{
m_vpTextContainers [ Index . m_Index ] - > Reset ( ) ;
FreeTextContainerIndex ( Index ) ;
}
2020-08-20 06:54:59 +00:00
2023-07-17 13:49:41 +00:00
STextContainer & GetTextContainer ( const STextContainerIndex & Index )
{
dbg_assert ( Index . Valid ( ) , " Text container index was invalid. " ) ;
if ( Index . m_Index > = ( int ) m_vpTextContainers . size ( ) )
{
for ( int i = 0 ; i < Index . m_Index + 1 - ( int ) m_vpTextContainers . size ( ) ; + + i )
m_vpTextContainers . push_back ( new STextContainer ( ) ) ;
2019-03-28 21:38:20 +00:00
}
2023-07-17 13:49:41 +00:00
if ( m_vpTextContainers [ Index . m_Index ] - > m_ContainerIndex . m_UseCount . get ( ) ! = Index . m_UseCount . get ( ) )
2010-05-29 07:25:38 +00:00
{
2023-07-17 13:49:41 +00:00
m_vpTextContainers [ Index . m_Index ] - > m_ContainerIndex = Index ;
2010-05-29 07:25:38 +00:00
}
2023-07-17 13:49:41 +00:00
return * m_vpTextContainers [ Index . m_Index ] ;
}
2010-05-29 07:25:38 +00:00
2023-07-17 13:49:41 +00:00
int WordLength ( const char * pText ) const
{
const char * pCursor = pText ;
while ( true )
2021-09-12 17:40:23 +00:00
{
2023-07-17 13:49:41 +00:00
if ( * pCursor = = ' \0 ' )
return pCursor - pText ;
if ( * pCursor = = ' \n ' | | * pCursor = = ' \t ' | | * pCursor = = ' ' )
return pCursor - pText + 1 ;
str_utf8_decode ( & pCursor ) ;
2021-09-12 17:40:23 +00:00
}
2023-07-17 13:49:41 +00:00
}
2018-03-13 20:49:07 +00:00
2023-07-17 13:49:41 +00:00
bool LoadFontCollection ( const char * pFontName , const FT_Byte * pFontData , FT_Long FontDataSize )
{
FT_Face FtFace ;
FT_Error CollectionLoadError = FT_New_Memory_Face ( m_FTLibrary , pFontData , FontDataSize , - 1 , & FtFace ) ;
if ( CollectionLoadError )
2018-03-13 20:49:07 +00:00
{
2023-07-17 13:49:41 +00:00
char aBuf [ 256 ] ;
str_format ( aBuf , sizeof ( aBuf ) , " Failed to load font file '%s': %s " , pFontName , FT_Error_String ( CollectionLoadError ) ) ;
Console ( ) - > Print ( IConsole : : OUTPUT_LEVEL_STANDARD , " textrender " , aBuf ) ;
return false ;
}
2011-04-13 18:37:12 +00:00
2023-07-17 13:49:41 +00:00
const FT_Long NumFaces = FtFace - > num_faces ;
FT_Done_Face ( FtFace ) ;
2020-09-14 14:42:33 +00:00
2023-07-17 13:49:41 +00:00
bool LoadedAny = false ;
for ( FT_Long FaceIndex = 0 ; FaceIndex < NumFaces ; + + FaceIndex )
{
FT_Error FaceLoadError = FT_New_Memory_Face ( m_FTLibrary , pFontData , FontDataSize , FaceIndex , & FtFace ) ;
if ( FaceLoadError )
2021-09-12 17:40:23 +00:00
{
2023-07-17 13:49:41 +00:00
char aBuf [ 256 ] ;
str_format ( aBuf , sizeof ( aBuf ) , " Failed to load font face %ld from font file '%s': %s " , FaceIndex , pFontName , FT_Error_String ( FaceLoadError ) ) ;
Console ( ) - > Print ( IConsole : : OUTPUT_LEVEL_STANDARD , " textrender " , aBuf ) ;
FT_Done_Face ( FtFace ) ;
continue ;
2021-09-12 17:40:23 +00:00
}
2023-07-17 13:49:41 +00:00
m_pGlyphMap - > AddFace ( FtFace ) ;
2021-09-12 17:40:23 +00:00
2023-07-17 13:49:41 +00:00
char aBuf [ 256 ] ;
str_format ( aBuf , sizeof ( aBuf ) , " Loaded font face %ld '%s %s' from font file '%s' " , FaceIndex , FtFace - > family_name , FtFace - > style_name , pFontName ) ;
Console ( ) - > Print ( IConsole : : OUTPUT_LEVEL_ADDINFO , " textrender " , aBuf ) ;
LoadedAny = true ;
2010-05-29 07:25:38 +00:00
}
2011-04-13 18:37:12 +00:00
2023-07-17 13:49:41 +00:00
if ( ! LoadedAny )
2010-05-29 07:25:38 +00:00
{
2023-07-17 13:49:41 +00:00
char aBuf [ 256 ] ;
str_format ( aBuf , sizeof ( aBuf ) , " Failed to load font file '%s': no font faces could be loaded " , pFontName ) ;
Console ( ) - > Print ( IConsole : : OUTPUT_LEVEL_STANDARD , " textrender " , aBuf ) ;
return false ;
2010-05-29 07:25:38 +00:00
}
2023-07-17 13:49:41 +00:00
return true ;
2010-05-29 07:25:38 +00:00
}
2023-07-17 13:49:41 +00:00
void SetRenderFlags ( unsigned Flags ) override
2010-05-29 07:25:38 +00:00
{
2023-07-17 13:49:41 +00:00
m_RenderFlags = Flags ;
2010-05-29 07:25:38 +00:00
}
2019-04-26 19:36:49 +00:00
2023-07-17 13:49:41 +00:00
unsigned GetRenderFlags ( ) const override
2010-05-29 07:25:38 +00:00
{
2023-07-17 13:49:41 +00:00
return m_RenderFlags ;
2010-05-29 07:25:38 +00:00
}
2011-04-13 18:37:12 +00:00
2010-05-29 07:25:38 +00:00
public :
CTextRender ( )
{
2023-07-17 13:49:41 +00:00
m_pConsole = nullptr ;
2023-02-17 17:07:17 +00:00
m_pGraphics = nullptr ;
2023-07-17 13:49:41 +00:00
m_pStorage = nullptr ;
m_pGlyphMap = nullptr ;
2010-05-29 07:25:38 +00:00
2021-09-12 17:40:23 +00:00
m_Color = DefaultTextColor ( ) ;
m_OutlineColor = DefaultTextOutlineColor ( ) ;
2023-04-14 13:03:29 +00:00
m_SelectionColor = DefaultTextSelectionColor ( ) ;
2010-05-29 07:25:38 +00:00
2023-02-17 17:07:17 +00:00
m_FTLibrary = nullptr ;
2010-05-29 07:25:38 +00:00
2018-03-21 14:43:56 +00:00
m_RenderFlags = 0 ;
2022-06-13 16:07:29 +00:00
m_CursorRenderTime = time_get_nanoseconds ( ) ;
2010-05-29 07:25:38 +00:00
}
2011-04-13 18:37:12 +00:00
2022-05-17 18:33:27 +00:00
void Init ( ) override
2010-05-29 07:25:38 +00:00
{
2023-07-17 13:49:41 +00:00
m_pConsole = Kernel ( ) - > RequestInterface < IConsole > ( ) ;
2010-05-29 07:25:38 +00:00
m_pGraphics = Kernel ( ) - > RequestInterface < IGraphics > ( ) ;
2023-07-17 13:49:41 +00:00
m_pStorage = Kernel ( ) - > RequestInterface < IStorage > ( ) ;
2010-05-29 07:25:38 +00:00
FT_Init_FreeType ( & m_FTLibrary ) ;
2023-07-17 13:49:41 +00:00
m_pGlyphMap = new CGlyphMap ( m_pGraphics ) ;
2020-03-20 12:48:45 +00:00
// print freetype version
{
int LMajor , LMinor , LPatch ;
FT_Library_Version ( m_FTLibrary , & LMajor , & LMinor , & LPatch ) ;
2023-07-17 13:49:41 +00:00
char aFreetypeVersion [ 128 ] ;
str_format ( aFreetypeVersion , sizeof ( aFreetypeVersion ) , " Freetype version %d.%d.%d (compiled = %d.%d.%d) " , LMajor , LMinor , LPatch , FREETYPE_MAJOR , FREETYPE_MINOR , FREETYPE_PATCH ) ;
Console ( ) - > Print ( IConsole : : OUTPUT_LEVEL_STANDARD , " textrender " , aFreetypeVersion ) ;
2020-03-20 12:48:45 +00:00
}
2018-03-13 20:49:07 +00:00
m_FirstFreeTextContainerIndex = - 1 ;
m_DefaultTextContainerInfo . m_Stride = sizeof ( STextCharQuadVertex ) ;
2022-03-20 17:03:25 +00:00
m_DefaultTextContainerInfo . m_VertBufferBindingIndex = - 1 ;
2018-03-13 20:49:07 +00:00
2022-06-11 19:38:18 +00:00
m_DefaultTextContainerInfo . m_vAttributes . emplace_back ( ) ;
SBufferContainerInfo : : SAttribute * pAttr = & m_DefaultTextContainerInfo . m_vAttributes . back ( ) ;
2018-03-13 20:49:07 +00:00
pAttr - > m_DataTypeCount = 2 ;
pAttr - > m_FuncType = 0 ;
pAttr - > m_Normalized = false ;
2023-02-17 17:07:17 +00:00
pAttr - > m_pOffset = nullptr ;
2018-03-13 20:49:07 +00:00
pAttr - > m_Type = GRAPHICS_TYPE_FLOAT ;
2023-02-17 17:07:17 +00:00
2022-06-11 19:38:18 +00:00
m_DefaultTextContainerInfo . m_vAttributes . emplace_back ( ) ;
pAttr = & m_DefaultTextContainerInfo . m_vAttributes . back ( ) ;
2018-03-13 20:49:07 +00:00
pAttr - > m_DataTypeCount = 2 ;
pAttr - > m_FuncType = 0 ;
pAttr - > m_Normalized = false ;
2020-09-26 19:41:58 +00:00
pAttr - > m_pOffset = ( void * ) ( sizeof ( float ) * 2 ) ;
2018-03-13 20:49:07 +00:00
pAttr - > m_Type = GRAPHICS_TYPE_FLOAT ;
2023-02-17 17:07:17 +00:00
2022-06-11 19:38:18 +00:00
m_DefaultTextContainerInfo . m_vAttributes . emplace_back ( ) ;
pAttr = & m_DefaultTextContainerInfo . m_vAttributes . back ( ) ;
2018-03-13 20:49:07 +00:00
pAttr - > m_DataTypeCount = 4 ;
pAttr - > m_FuncType = 0 ;
pAttr - > m_Normalized = true ;
2020-09-26 19:41:58 +00:00
pAttr - > m_pOffset = ( void * ) ( sizeof ( float ) * 2 + sizeof ( float ) * 2 ) ;
2018-03-13 20:49:07 +00:00
pAttr - > m_Type = GRAPHICS_TYPE_UNSIGNED_BYTE ;
2010-05-29 07:25:38 +00:00
}
2019-04-26 19:36:49 +00:00
2023-07-17 13:49:41 +00:00
void Shutdown ( ) override
2010-05-29 07:25:38 +00:00
{
2023-07-17 13:49:41 +00:00
for ( auto * pTextCont : m_vpTextContainers )
delete pTextCont ;
m_vpTextContainers . clear ( ) ;
2011-04-13 18:37:12 +00:00
2023-07-17 13:49:41 +00:00
delete m_pGlyphMap ;
m_pGlyphMap = nullptr ;
2011-04-13 18:37:12 +00:00
2023-07-17 13:49:41 +00:00
if ( m_FTLibrary ! = nullptr )
FT_Done_FreeType ( m_FTLibrary ) ;
m_FTLibrary = nullptr ;
2010-05-29 07:25:38 +00:00
2023-07-17 13:49:41 +00:00
for ( auto * pFontData : m_vpFontData )
free ( pFontData ) ;
m_vpFontData . clear ( ) ;
2018-03-13 20:49:07 +00:00
2023-07-17 13:49:41 +00:00
m_DefaultTextContainerInfo . m_vAttributes . clear ( ) ;
m_pConsole = nullptr ;
m_pGraphics = nullptr ;
m_pStorage = nullptr ;
}
2023-02-17 17:07:17 +00:00
2023-07-17 13:49:41 +00:00
void LoadFonts ( ) override
{
// read file data into buffer
const char * pFilename = " fonts/index.json " ;
void * pFileData ;
unsigned JsonFileSize ;
if ( ! Storage ( ) - > ReadFile ( pFilename , IStorage : : TYPE_ALL , & pFileData , & JsonFileSize ) )
2023-02-17 17:07:17 +00:00
{
2023-07-17 13:49:41 +00:00
char aBuf [ 256 ] ;
str_format ( aBuf , sizeof ( aBuf ) , " Failed to open/read font index file '%s' " , pFilename ) ;
Console ( ) - > Print ( IConsole : : OUTPUT_LEVEL_STANDARD , " textrender " , aBuf ) ;
return ;
2023-02-17 17:07:17 +00:00
}
2018-03-13 20:49:07 +00:00
2023-07-17 13:49:41 +00:00
// parse json data
json_settings JsonSettings { } ;
char aError [ 256 ] ;
json_value * pJsonData = json_parse_ex ( & JsonSettings , static_cast < const json_char * > ( pFileData ) , JsonFileSize , aError ) ;
free ( pFileData ) ;
if ( pJsonData = = nullptr )
{
char aBuf [ 512 ] ;
str_format ( aBuf , sizeof ( aBuf ) , " Failed to parse font index file '%s': %s " , pFilename , aError ) ;
Console ( ) - > Print ( IConsole : : OUTPUT_LEVEL_STANDARD , " textrender " , aBuf ) ;
return ;
}
2018-03-13 20:49:07 +00:00
2023-07-17 13:49:41 +00:00
// extract font file definitions
const json_value & FontFiles = ( * pJsonData ) [ " font files " ] ;
if ( FontFiles . type = = json_array )
{
for ( unsigned FontFileIndex = 0 ; FontFileIndex < FontFiles . u . array . length ; + + FontFileIndex )
{
if ( FontFiles [ FontFileIndex ] . type ! = json_string )
continue ;
2020-08-20 09:22:25 +00:00
2023-07-17 13:49:41 +00:00
char aFontName [ IO_MAX_PATH_LENGTH ] ;
str_format ( aFontName , sizeof ( aFontName ) , " fonts/%s " , FontFiles [ FontFileIndex ] . u . string . ptr ) ;
void * pFontData ;
unsigned FontDataSize ;
if ( Storage ( ) - > ReadFile ( aFontName , IStorage : : TYPE_ALL , & pFontData , & FontDataSize ) )
{
if ( LoadFontCollection ( aFontName , static_cast < FT_Byte * > ( pFontData ) , ( FT_Long ) FontDataSize ) )
{
m_vpFontData . push_back ( pFontData ) ;
}
else
{
free ( pFontData ) ;
}
}
}
}
2020-08-20 09:22:25 +00:00
2023-07-17 13:49:41 +00:00
// extract default family name
const json_value & DefaultFace = ( * pJsonData ) [ " default " ] ;
if ( DefaultFace . type = = json_string )
2020-08-20 09:22:25 +00:00
{
2023-07-17 13:49:41 +00:00
m_pGlyphMap - > SetDefaultFaceByName ( DefaultFace . u . string . ptr ) ;
2020-08-20 09:22:25 +00:00
}
2023-07-17 13:49:41 +00:00
// extract language variant family names
const json_value & Variants = ( * pJsonData ) [ " language variants " ] ;
if ( Variants . type = = json_object )
{
m_vVariants . resize ( Variants . u . object . length ) ;
for ( size_t i = 0 ; i < Variants . u . object . length ; + + i )
{
str_format ( m_vVariants [ i ] . m_aLanguageFile , sizeof ( m_vVariants [ i ] . m_aLanguageFile ) , " languages/%s.txt " , Variants . u . object . values [ i ] . name ) ;
2019-04-26 19:36:49 +00:00
2023-07-17 13:49:41 +00:00
const json_value * pFamilyName = Variants . u . object . values [ i ] . value ;
if ( pFamilyName - > type = = json_string )
str_copy ( m_vVariants [ i ] . m_aFamilyName , pFamilyName - > u . string . ptr ) ;
else
m_vVariants [ i ] . m_aFamilyName [ 0 ] = ' \0 ' ;
}
}
2018-03-13 20:49:07 +00:00
2023-07-17 13:49:41 +00:00
// extract fallback family names
const json_value & FallbackFaces = ( * pJsonData ) [ " fallbacks " ] ;
if ( FallbackFaces . type = = json_array )
{
for ( unsigned i = 0 ; i < FallbackFaces . u . array . length ; + + i )
{
if ( FallbackFaces [ i ] . type = = json_string )
{
m_pGlyphMap - > AddFallbackFaceByName ( FallbackFaces [ i ] . u . string . ptr ) ;
}
}
}
2018-03-13 20:49:07 +00:00
2023-07-17 13:49:41 +00:00
// extract icon font family name
const json_value & IconFace = ( * pJsonData ) [ " icon " ] ;
if ( IconFace . type = = json_string )
2018-03-13 20:49:07 +00:00
{
2023-07-17 13:49:41 +00:00
m_pGlyphMap - > SetIconFaceByName ( IconFace . u . string . ptr ) ;
2018-03-13 20:49:07 +00:00
}
2023-07-17 13:49:41 +00:00
json_value_free ( pJsonData ) ;
2018-03-13 20:49:07 +00:00
}
2010-05-29 07:25:38 +00:00
2023-07-17 13:49:41 +00:00
void SetFontPreset ( EFontPreset FontPreset ) override
2010-05-29 07:25:38 +00:00
{
2023-07-17 13:49:41 +00:00
m_pGlyphMap - > SetFontPreset ( FontPreset ) ;
2010-05-29 07:25:38 +00:00
}
2011-04-13 18:37:12 +00:00
2023-07-17 13:49:41 +00:00
void SetFontLanguageVariant ( const char * pLanguageFile ) override
2018-03-13 20:49:07 +00:00
{
2023-07-17 13:49:41 +00:00
for ( const auto & Variant : m_vVariants )
{
if ( str_comp ( pLanguageFile , Variant . m_aLanguageFile ) = = 0 )
{
m_pGlyphMap - > SetVariantFaceByName ( Variant . m_aFamilyName ) ;
return ;
}
}
m_pGlyphMap - > SetVariantFaceByName ( nullptr ) ;
2018-03-13 20:49:07 +00:00
}
2011-04-13 18:37:12 +00:00
2023-02-17 17:07:17 +00:00
void SetCursor ( CTextCursor * pCursor , float x , float y , float FontSize , int Flags ) const override
2010-05-29 07:25:38 +00:00
{
2023-07-17 13:49:41 +00:00
pCursor - > m_Flags = Flags ;
pCursor - > m_LineCount = 1 ;
pCursor - > m_GlyphCount = 0 ;
pCursor - > m_CharCount = 0 ;
pCursor - > m_MaxLines = 0 ;
2023-12-11 21:14:45 +00:00
2023-10-13 17:36:00 +00:00
pCursor - > m_LineSpacing = 0 ;
2023-12-11 21:14:45 +00:00
pCursor - > m_AlignedLineSpacing = 0 ;
2023-07-17 13:49:41 +00:00
2010-05-29 07:25:38 +00:00
pCursor - > m_StartX = x ;
pCursor - > m_StartY = y ;
2023-07-17 13:49:41 +00:00
pCursor - > m_LineWidth = - 1.0f ;
2010-05-29 07:25:38 +00:00
pCursor - > m_X = x ;
pCursor - > m_Y = y ;
2023-07-17 13:49:41 +00:00
pCursor - > m_MaxCharacterHeight = 0.0f ;
pCursor - > m_LongestLineWidth = 0.0f ;
pCursor - > m_FontSize = FontSize ;
pCursor - > m_AlignedFontSize = FontSize ;
2021-09-12 17:40:23 +00:00
pCursor - > m_CalculateSelectionMode = TEXT_CURSOR_SELECTION_MODE_NONE ;
2023-04-14 13:02:55 +00:00
pCursor - > m_SelectionHeightFactor = 1.0f ;
Port line input and IME support from 0.7
Port the line input (UI edit boxes, chat, console) and Input Method Editor (IME) support from upstream. Closes #4397.
General
------------------------------
Fix issues with the text input. Closes #4346. Closes #4524.
Word skipping (when holding Ctrl) is overhauled to be consistent with the Windows / Firefox experience that I took as reference.
Improve usability by not blinking (i.e. always rendering) the caret shortly after is has been moved.
UI text input
------------------------------
Fix inconsistent mouse-based left and right scrolling (closes #4347).
Support smooth left and right scrolling.
Chat
------------------------------
Support keyboard-based text selection of the chat input.
Mouse-based selection could be support in the future when we decide to add something like an ingame UI cursor.
Support smooth up and down scrolling of the chat input, removing the old hack that offsets the input string to simulate scrolling.
Console
------------------------------
Also support mouse-based text selection of the command input.
Only text from either the command input or the console log can be selected at the same time. This ensures that Ctrl+C will always copy the text that is currently visually selected in the console.
Check for Ctrl+C input event in event handler instead of in render function, to hopefully fix the issue that copying does not work sometimes (closes #5974 until further notice).
When Ctrl+C is used to copy text from the console log, the selection is cleared. This should make it more clear when text was copied from the log.
Fix an issue that was preventing the console log selection from being cleared, when all log lines are selected.
Remove Ctrl+A/E hotkeys that move cursor to beginning/end respectively. Ctrl+A now selectes all text like for all other inputs. Home and End keys can still be used to go the beginning and end.
Remove Ctrl+U/K hotkeys that clear everything before/after the cursor respectively. Hold shift and use Home/End to select everything instead.
IME support
------------------------------
Render list of IME candidates in the client on Windows, so the candidate list can also be viewed in fullscreen mode. There is no API available to retrieve a candidate list on the other operating systems.
Improve composition rendering by underlining the composition text instead of putting it in square brackets.
Track active input globally to properly activate and deactivate IME through the SDL functions.
Closes #1030. Closes #1008.
Password rendering
------------------------------
Fix rendering of passwords containing unicode. Instead of rendering one star character for each UTF-8 `char`, render on star for every unicode codepoint.
Show the composition text also for passwords. Without seeing the composition text it's hard to type a password containing those characters. The candidate window exposes the composition anyway. If you don't want to expose your password this way, e.g. while streaming, you could:
1. Use a latin password and switch off the IME for the password input with the IME hotkey.
2. Blank your screen with an external program while you are streaming and entering passwords.
3. Use binds to authenticate in rcon or to set the server browser password.
Refactoring
------------------------------
Move all text input logic and general rendering to `CLineInput`.
A `CLineInput` is associated with a particular `char` buffer given as a pointer either in the constructor or with `SetBuffer`. The maximum byte size of the buffer must also be specified. The maximum length in unicode codepoints can also be specified separately (e.g. on upstream, name are limited by the number of unicode codepoints instead).
Add `CLineInputBuffered`, which is a `CLineInput` that own a `char` buffer of a fixed size, which is specified as a template argument. As `CLineInput` does not own a buffer anymore, this reduces duplicate code for line inputs that need their own buffer.
Add `CLineInputNumber` which has additional convenience functions to consider the text as an `int` or `float`, to reduce duplicate code in those cases. In the future we could also add an input filter function so that only numbers can be entered in the number input.
Add `CLineInput::SetClipboardLineCallback` to handle the case that multiple lines of text are pasted into a lineinput. This reduces duplicate code, as this behavior was previously implemented separately for chat and console. The behavior is also fixed to be consistent with the console on Windows, so the first line being pasted edits the current input text and then sends it instead of being sent on its own without the existing input text.
Add `CalcFontSizeAndBoundingBox` to UI to reduce duplicate code. Expose `CalcAlignedCursorPos` as static member function to reuse it for line input.
Dispatch input events to UI inputs through the event handler instead of storing them in a duplicate buffer.
Use `size_t` for line input cursor position, length etc. and for `str_utf8_stats`.
Add `IButtonColorFunction` to UI to describe a functions that defines colors for the Default, Active and Hovered states of UI elements. Add some default button color functions. Use button color function to reduce duplicate code in scrollbar rendering.
Use `vec2` instead of two `floats` to represent the mouse positions in the text renderer.
Remove `CaretPosition` again, as it does not calculate the correct Y position near line breaks due to the wrapping being different when not rendering the entire string. Instead, calculate the exact caret position when rending a text container and store the caret position in the text cursor for later use.
IME usage guide (Windows)
------------------------------
1. Install the respective language and the Microsoft-IME keyboard (e.g. for Chinese, Japanese or Korean).
2. Launch the game (or a text editor to first try out the IME). Note that Windows may track the input language separately for every application. You can change this in the Windows input settings so the input language is changed globally.
2. Switch the input language using the hotkey Windows+Space or another hotkey that you configured in the Windows input settings (Alt+Shift is the default, but you should consider disabling it, to avoid accidentally changing the input language while playing).
3. Switch from Latin/English input mode to the respective asian input mode.
- Chinese: Use Ctrl+Space to switch between English and Chinese input mode. You can change this hotkey in the IME's settings.
- Japanese: Use Ctrl+Space to switch between Alphanumeric and Hiragana/Katakana input mode. You can change this hotkey in the IME's settings.
- Korean: Use Right Alt to switch between English and Hangul input mode. You cannot change this hotkey as of yet.
- Note that the input mode is also tracked per application, but there is no setting to change this behavior as far as I know, so you'll need to switch for every application separately.
4. Start typing. The underlined text is the current composition text. While a composition is active, you can only edit the composition text. Confirm the composition with Space or by selecting a candidate from the candidate list with the arrow keys. Cancel the composition with Escape or by using Backspace to delete the composition text. Note that not all languages offer a candidate list.
SDL version-specific issues
------------------------------
- 2.26.5, 2.24.2, 2.0.22: IME candidates work. But there are minor bugs when moving the composition cursor.
- 2.0.18, 2.0.20: IME candidates work.
- 2.0.16 (our current version): IME candidates cannot be determined with Windows API. Windows tries to draw the composition window like before, so this does not work in fullscreen mode.
- 2.0.8 (upstream 0.7): IME candidates work. But this SDL version is too old for us.
2023-01-03 21:28:38 +00:00
pCursor - > m_PressMouse = vec2 ( 0.0f , 0.0f ) ;
pCursor - > m_ReleaseMouse = vec2 ( 0.0f , 0.0f ) ;
2021-09-12 17:40:23 +00:00
pCursor - > m_SelectionStart = 0 ;
pCursor - > m_SelectionEnd = 0 ;
2021-09-16 14:50:17 +00:00
pCursor - > m_CursorMode = TEXT_CURSOR_CURSOR_MODE_NONE ;
2023-04-14 13:05:34 +00:00
pCursor - > m_ForceCursorRendering = false ;
2021-09-16 14:50:17 +00:00
pCursor - > m_CursorCharacter = - 1 ;
Port line input and IME support from 0.7
Port the line input (UI edit boxes, chat, console) and Input Method Editor (IME) support from upstream. Closes #4397.
General
------------------------------
Fix issues with the text input. Closes #4346. Closes #4524.
Word skipping (when holding Ctrl) is overhauled to be consistent with the Windows / Firefox experience that I took as reference.
Improve usability by not blinking (i.e. always rendering) the caret shortly after is has been moved.
UI text input
------------------------------
Fix inconsistent mouse-based left and right scrolling (closes #4347).
Support smooth left and right scrolling.
Chat
------------------------------
Support keyboard-based text selection of the chat input.
Mouse-based selection could be support in the future when we decide to add something like an ingame UI cursor.
Support smooth up and down scrolling of the chat input, removing the old hack that offsets the input string to simulate scrolling.
Console
------------------------------
Also support mouse-based text selection of the command input.
Only text from either the command input or the console log can be selected at the same time. This ensures that Ctrl+C will always copy the text that is currently visually selected in the console.
Check for Ctrl+C input event in event handler instead of in render function, to hopefully fix the issue that copying does not work sometimes (closes #5974 until further notice).
When Ctrl+C is used to copy text from the console log, the selection is cleared. This should make it more clear when text was copied from the log.
Fix an issue that was preventing the console log selection from being cleared, when all log lines are selected.
Remove Ctrl+A/E hotkeys that move cursor to beginning/end respectively. Ctrl+A now selectes all text like for all other inputs. Home and End keys can still be used to go the beginning and end.
Remove Ctrl+U/K hotkeys that clear everything before/after the cursor respectively. Hold shift and use Home/End to select everything instead.
IME support
------------------------------
Render list of IME candidates in the client on Windows, so the candidate list can also be viewed in fullscreen mode. There is no API available to retrieve a candidate list on the other operating systems.
Improve composition rendering by underlining the composition text instead of putting it in square brackets.
Track active input globally to properly activate and deactivate IME through the SDL functions.
Closes #1030. Closes #1008.
Password rendering
------------------------------
Fix rendering of passwords containing unicode. Instead of rendering one star character for each UTF-8 `char`, render on star for every unicode codepoint.
Show the composition text also for passwords. Without seeing the composition text it's hard to type a password containing those characters. The candidate window exposes the composition anyway. If you don't want to expose your password this way, e.g. while streaming, you could:
1. Use a latin password and switch off the IME for the password input with the IME hotkey.
2. Blank your screen with an external program while you are streaming and entering passwords.
3. Use binds to authenticate in rcon or to set the server browser password.
Refactoring
------------------------------
Move all text input logic and general rendering to `CLineInput`.
A `CLineInput` is associated with a particular `char` buffer given as a pointer either in the constructor or with `SetBuffer`. The maximum byte size of the buffer must also be specified. The maximum length in unicode codepoints can also be specified separately (e.g. on upstream, name are limited by the number of unicode codepoints instead).
Add `CLineInputBuffered`, which is a `CLineInput` that own a `char` buffer of a fixed size, which is specified as a template argument. As `CLineInput` does not own a buffer anymore, this reduces duplicate code for line inputs that need their own buffer.
Add `CLineInputNumber` which has additional convenience functions to consider the text as an `int` or `float`, to reduce duplicate code in those cases. In the future we could also add an input filter function so that only numbers can be entered in the number input.
Add `CLineInput::SetClipboardLineCallback` to handle the case that multiple lines of text are pasted into a lineinput. This reduces duplicate code, as this behavior was previously implemented separately for chat and console. The behavior is also fixed to be consistent with the console on Windows, so the first line being pasted edits the current input text and then sends it instead of being sent on its own without the existing input text.
Add `CalcFontSizeAndBoundingBox` to UI to reduce duplicate code. Expose `CalcAlignedCursorPos` as static member function to reuse it for line input.
Dispatch input events to UI inputs through the event handler instead of storing them in a duplicate buffer.
Use `size_t` for line input cursor position, length etc. and for `str_utf8_stats`.
Add `IButtonColorFunction` to UI to describe a functions that defines colors for the Default, Active and Hovered states of UI elements. Add some default button color functions. Use button color function to reduce duplicate code in scrollbar rendering.
Use `vec2` instead of two `floats` to represent the mouse positions in the text renderer.
Remove `CaretPosition` again, as it does not calculate the correct Y position near line breaks due to the wrapping being different when not rendering the entire string. Instead, calculate the exact caret position when rending a text container and store the caret position in the text cursor for later use.
IME usage guide (Windows)
------------------------------
1. Install the respective language and the Microsoft-IME keyboard (e.g. for Chinese, Japanese or Korean).
2. Launch the game (or a text editor to first try out the IME). Note that Windows may track the input language separately for every application. You can change this in the Windows input settings so the input language is changed globally.
2. Switch the input language using the hotkey Windows+Space or another hotkey that you configured in the Windows input settings (Alt+Shift is the default, but you should consider disabling it, to avoid accidentally changing the input language while playing).
3. Switch from Latin/English input mode to the respective asian input mode.
- Chinese: Use Ctrl+Space to switch between English and Chinese input mode. You can change this hotkey in the IME's settings.
- Japanese: Use Ctrl+Space to switch between Alphanumeric and Hiragana/Katakana input mode. You can change this hotkey in the IME's settings.
- Korean: Use Right Alt to switch between English and Hangul input mode. You cannot change this hotkey as of yet.
- Note that the input mode is also tracked per application, but there is no setting to change this behavior as far as I know, so you'll need to switch for every application separately.
4. Start typing. The underlined text is the current composition text. While a composition is active, you can only edit the composition text. Confirm the composition with Space or by selecting a candidate from the candidate list with the arrow keys. Cancel the composition with Escape or by using Backspace to delete the composition text. Note that not all languages offer a candidate list.
SDL version-specific issues
------------------------------
- 2.26.5, 2.24.2, 2.0.22: IME candidates work. But there are minor bugs when moving the composition cursor.
- 2.0.18, 2.0.20: IME candidates work.
- 2.0.16 (our current version): IME candidates cannot be determined with Windows API. Windows tries to draw the composition window like before, so this does not work in fullscreen mode.
- 2.0.8 (upstream 0.7): IME candidates work. But this SDL version is too old for us.
2023-01-03 21:28:38 +00:00
pCursor - > m_CursorRenderedPosition = vec2 ( - 1.0f , - 1.0f ) ;
2023-12-02 18:29:31 +00:00
pCursor - > m_vColorSplits = { } ;
2020-10-13 20:08:52 +00:00
}
2023-02-17 17:07:17 +00:00
void MoveCursor ( CTextCursor * pCursor , float x , float y ) const override
2020-10-13 20:08:52 +00:00
{
pCursor - > m_X + = x ;
pCursor - > m_Y + = y ;
2010-05-29 07:25:38 +00:00
}
2019-04-26 19:36:49 +00:00
2023-02-17 17:07:17 +00:00
void SetCursorPosition ( CTextCursor * pCursor , float x , float y ) const override
2020-10-29 00:55:01 +00:00
{
pCursor - > m_X = x ;
pCursor - > m_Y = y ;
}
2023-04-12 20:36:52 +00:00
void Text ( float x , float y , float Size , const char * pText , float LineWidth = - 1.0f ) override
2010-05-29 07:25:38 +00:00
{
CTextCursor Cursor ;
SetCursor ( & Cursor , x , y , Size , TEXTFLAG_RENDER ) ;
2020-07-15 19:42:48 +00:00
Cursor . m_LineWidth = LineWidth ;
2010-05-29 07:25:38 +00:00
TextEx ( & Cursor , pText , - 1 ) ;
}
2023-05-08 15:25:26 +00:00
float TextWidth ( float Size , const char * pText , int StrLength = - 1 , float LineWidth = - 1.0f , int Flags = 0 , const STextSizeProperties & TextSizeProps = { } ) override
2010-05-29 07:25:38 +00:00
{
CTextCursor Cursor ;
2023-04-09 15:28:58 +00:00
SetCursor ( & Cursor , 0 , 0 , Size , Flags ) ;
2020-07-15 19:10:13 +00:00
Cursor . m_LineWidth = LineWidth ;
TextEx ( & Cursor , pText , StrLength ) ;
2023-05-08 15:25:26 +00:00
if ( TextSizeProps . m_pHeight ! = nullptr )
* TextSizeProps . m_pHeight = Cursor . Height ( ) ;
if ( TextSizeProps . m_pAlignedFontSize ! = nullptr )
* TextSizeProps . m_pAlignedFontSize = Cursor . m_AlignedFontSize ;
if ( TextSizeProps . m_pMaxCharacterHeightInLine ! = nullptr )
* TextSizeProps . m_pMaxCharacterHeightInLine = Cursor . m_MaxCharacterHeight ;
if ( TextSizeProps . m_pLineCount ! = nullptr )
* TextSizeProps . m_pLineCount = Cursor . m_LineCount ;
2023-03-15 12:43:14 +00:00
return Cursor . m_LongestLineWidth ;
2010-05-29 07:25:38 +00:00
}
2011-04-13 18:37:12 +00:00
2023-10-13 17:36:00 +00:00
STextBoundingBox TextBoundingBox ( float Size , const char * pText , int StrLength = - 1 , float LineWidth = - 1.0f , float LineSpacing = 0.0f , int Flags = 0 ) override
2023-04-10 09:04:28 +00:00
{
CTextCursor Cursor ;
SetCursor ( & Cursor , 0 , 0 , Size , Flags ) ;
Cursor . m_LineWidth = LineWidth ;
2023-10-13 17:36:00 +00:00
Cursor . m_LineSpacing = LineSpacing ;
2023-04-10 09:04:28 +00:00
TextEx ( & Cursor , pText , StrLength ) ;
return Cursor . BoundingBox ( ) ;
}
2022-05-17 18:33:27 +00:00
void TextColor ( float r , float g , float b , float a ) override
2010-05-29 07:25:38 +00:00
{
2019-04-26 22:34:20 +00:00
m_Color . r = r ;
m_Color . g = g ;
m_Color . b = b ;
m_Color . a = a ;
2010-05-29 07:25:38 +00:00
}
2023-02-17 17:07:17 +00:00
void TextColor ( ColorRGBA rgb ) override
{
m_Color = rgb ;
}
2011-03-13 11:55:00 +00:00
2022-05-17 18:33:27 +00:00
void TextOutlineColor ( float r , float g , float b , float a ) override
2011-03-13 11:55:00 +00:00
{
2019-04-26 22:34:20 +00:00
m_OutlineColor . r = r ;
m_OutlineColor . g = g ;
m_OutlineColor . b = b ;
m_OutlineColor . a = a ;
2011-03-13 11:55:00 +00:00
}
2023-02-17 17:07:17 +00:00
void TextOutlineColor ( ColorRGBA rgb ) override
{
m_OutlineColor = rgb ;
}
2011-04-13 18:37:12 +00:00
2022-05-17 18:33:27 +00:00
void TextSelectionColor ( float r , float g , float b , float a ) override
2021-09-12 17:40:23 +00:00
{
m_SelectionColor . r = r ;
m_SelectionColor . g = g ;
m_SelectionColor . b = b ;
m_SelectionColor . a = a ;
}
2023-02-17 17:07:17 +00:00
void TextSelectionColor ( ColorRGBA rgb ) override
{
m_SelectionColor = rgb ;
}
ColorRGBA GetTextColor ( ) const override
{
return m_Color ;
}
ColorRGBA GetTextOutlineColor ( ) const override
{
return m_OutlineColor ;
}
ColorRGBA GetTextSelectionColor ( ) const override
{
return m_SelectionColor ;
}
2020-11-08 18:41:16 +00:00
2023-04-12 20:36:52 +00:00
void TextEx ( CTextCursor * pCursor , const char * pText , int Length = - 1 ) override
2010-05-29 07:25:38 +00:00
{
2023-02-17 17:07:17 +00:00
const unsigned OldRenderFlags = m_RenderFlags ;
2022-03-20 17:03:25 +00:00
m_RenderFlags | = TEXT_RENDER_FLAG_ONE_TIME_USE ;
2023-05-05 13:58:17 +00:00
STextContainerIndex TextCont ;
2022-07-08 16:51:19 +00:00
CreateTextContainer ( TextCont , pCursor , pText , Length ) ;
2022-03-20 17:03:25 +00:00
m_RenderFlags = OldRenderFlags ;
2023-05-05 13:58:17 +00:00
if ( TextCont . Valid ( ) )
2018-03-13 20:49:07 +00:00
{
2021-09-12 17:40:23 +00:00
if ( ( pCursor - > m_Flags & TEXTFLAG_RENDER ) ! = 0 )
2018-03-13 20:49:07 +00:00
{
2022-07-01 04:42:36 +00:00
ColorRGBA TextColor = DefaultTextColor ( ) ;
ColorRGBA TextColorOutline = DefaultTextOutlineColor ( ) ;
RenderTextContainer ( TextCont , TextColor , TextColorOutline ) ;
2018-03-13 20:49:07 +00:00
}
2021-09-12 17:40:23 +00:00
DeleteTextContainer ( TextCont ) ;
2018-03-13 20:49:07 +00:00
}
}
2023-05-05 13:58:17 +00:00
bool CreateTextContainer ( STextContainerIndex & TextContainerIndex , CTextCursor * pCursor , const char * pText , int Length = - 1 ) override
2018-03-13 20:49:07 +00:00
{
2023-05-05 13:58:17 +00:00
dbg_assert ( ! TextContainerIndex . Valid ( ) , " Text container index was not cleared. " ) ;
2021-09-12 17:40:23 +00:00
2023-07-17 13:49:41 +00:00
TextContainerIndex . Reset ( ) ;
2023-05-05 13:58:17 +00:00
TextContainerIndex . m_Index = GetFreeTextContainerIndex ( ) ;
2018-03-13 20:49:07 +00:00
2023-02-17 17:07:17 +00:00
float ScreenX0 , ScreenY0 , ScreenX1 , ScreenY1 ;
2018-03-13 20:49:07 +00:00
Graphics ( ) - > GetScreen ( & ScreenX0 , & ScreenY0 , & ScreenX1 , & ScreenY1 ) ;
2023-07-17 13:49:41 +00:00
STextContainer & TextContainer = GetTextContainer ( TextContainerIndex ) ;
TextContainer . m_SingleTimeUse = ( m_RenderFlags & TEXT_RENDER_FLAG_ONE_TIME_USE ) ! = 0 ;
const vec2 FakeToScreen = vec2 ( Graphics ( ) - > ScreenWidth ( ) / ( ScreenX1 - ScreenX0 ) , Graphics ( ) - > ScreenHeight ( ) / ( ScreenY1 - ScreenY0 ) ) ;
TextContainer . m_AlignedStartX = round_to_int ( pCursor - > m_X * FakeToScreen . x ) / FakeToScreen . x ;
TextContainer . m_AlignedStartY = round_to_int ( pCursor - > m_Y * FakeToScreen . y ) / FakeToScreen . y ;
2018-03-15 02:33:22 +00:00
TextContainer . m_X = pCursor - > m_X ;
TextContainer . m_Y = pCursor - > m_Y ;
2018-03-13 20:49:07 +00:00
TextContainer . m_Flags = pCursor - > m_Flags ;
2020-10-12 15:46:06 +00:00
if ( pCursor - > m_LineWidth < = 0 )
2023-07-17 13:49:41 +00:00
TextContainer . m_RenderFlags = m_RenderFlags | ETextRenderFlags : : TEXT_RENDER_FLAG_NO_FIRST_CHARACTER_X_BEARING | ETextRenderFlags : : TEXT_RENDER_FLAG_NO_LAST_CHARACTER_ADVANCE ;
else
TextContainer . m_RenderFlags = m_RenderFlags ;
2019-04-26 19:36:49 +00:00
2022-07-08 16:51:19 +00:00
AppendTextContainer ( TextContainerIndex , pCursor , pText , Length ) ;
2018-03-13 20:49:07 +00:00
2023-07-17 13:49:41 +00:00
const bool IsRendered = ( pCursor - > m_Flags & TEXTFLAG_RENDER ) ! = 0 ;
2022-06-11 19:38:18 +00:00
if ( TextContainer . m_StringInfo . m_vCharacterQuads . empty ( ) & & TextContainer . m_StringInfo . m_SelectionQuadContainerIndex = = - 1 & & IsRendered )
2018-03-13 20:49:07 +00:00
{
2022-07-08 16:51:19 +00:00
FreeTextContainer ( TextContainerIndex ) ;
return false ;
2018-03-13 20:49:07 +00:00
}
else
{
2022-06-11 19:38:18 +00:00
if ( Graphics ( ) - > IsTextBufferingEnabled ( ) & & IsRendered & & ! TextContainer . m_StringInfo . m_vCharacterQuads . empty ( ) )
2018-03-13 20:49:07 +00:00
{
2020-10-22 20:21:19 +00:00
if ( ( TextContainer . m_RenderFlags & TEXT_RENDER_FLAG_NO_AUTOMATIC_QUAD_UPLOAD ) = = 0 )
{
2022-07-08 16:51:19 +00:00
UploadTextContainer ( TextContainerIndex ) ;
2020-10-22 20:21:19 +00:00
}
2018-03-13 20:49:07 +00:00
}
2019-04-26 19:36:49 +00:00
2018-03-13 20:49:07 +00:00
TextContainer . m_LineCount = pCursor - > m_LineCount ;
2020-09-03 22:53:26 +00:00
TextContainer . m_GlyphCount = pCursor - > m_GlyphCount ;
2018-03-13 20:49:07 +00:00
TextContainer . m_CharCount = pCursor - > m_CharCount ;
TextContainer . m_MaxLines = pCursor - > m_MaxLines ;
TextContainer . m_LineWidth = pCursor - > m_LineWidth ;
2023-02-17 17:07:17 +00:00
return true ;
2018-03-13 20:49:07 +00:00
}
}
2023-05-05 13:58:17 +00:00
void AppendTextContainer ( STextContainerIndex TextContainerIndex , CTextCursor * pCursor , const char * pText , int Length = - 1 ) override
2018-03-13 20:49:07 +00:00
{
2020-09-26 19:41:58 +00:00
STextContainer & TextContainer = GetTextContainer ( TextContainerIndex ) ;
2023-06-13 19:06:48 +00:00
str_append ( TextContainer . m_aDebugText , pText ) ;
2018-03-13 20:49:07 +00:00
2023-02-17 17:07:17 +00:00
float ScreenX0 , ScreenY0 , ScreenX1 , ScreenY1 ;
2018-03-13 20:49:07 +00:00
Graphics ( ) - > GetScreen ( & ScreenX0 , & ScreenY0 , & ScreenX1 , & ScreenY1 ) ;
2023-07-17 13:49:41 +00:00
const vec2 FakeToScreen = vec2 ( Graphics ( ) - > ScreenWidth ( ) / ( ScreenX1 - ScreenX0 ) , Graphics ( ) - > ScreenHeight ( ) / ( ScreenY1 - ScreenY0 ) ) ;
const float CursorX = round_to_int ( pCursor - > m_X * FakeToScreen . x ) / FakeToScreen . x ;
const float CursorY = round_to_int ( pCursor - > m_Y * FakeToScreen . y ) / FakeToScreen . y ;
const int ActualSize = round_truncate ( pCursor - > m_FontSize * FakeToScreen . y ) ;
pCursor - > m_AlignedFontSize = ActualSize / FakeToScreen . y ;
2023-12-11 21:14:45 +00:00
pCursor - > m_AlignedLineSpacing = round_truncate ( pCursor - > m_LineSpacing * FakeToScreen . y ) / FakeToScreen . y ;
2018-03-13 20:49:07 +00:00
// string length
2020-10-12 10:29:47 +00:00
if ( Length < 0 )
Length = str_length ( pText ) ;
2023-04-12 20:36:10 +00:00
else
Length = minimum ( Length , str_length ( pText ) ) ;
2018-03-13 20:49:07 +00:00
2023-02-17 17:07:17 +00:00
const char * pCurrent = pText ;
2018-03-13 20:49:07 +00:00
const char * pEnd = pCurrent + Length ;
2022-05-31 10:20:45 +00:00
const char * pEllipsis = " … " ;
2023-07-17 13:49:41 +00:00
const SGlyph * pEllipsisGlyph = nullptr ;
2022-05-31 10:20:45 +00:00
if ( pCursor - > m_Flags & TEXTFLAG_ELLIPSIS_AT_END )
{
2023-02-17 17:13:20 +00:00
if ( pCursor - > m_LineWidth ! = - 1 & & pCursor - > m_LineWidth < TextWidth ( pCursor - > m_FontSize , pText , - 1 , - 1.0f ) )
2022-05-31 10:20:45 +00:00
{
2023-07-17 13:49:41 +00:00
pEllipsisGlyph = m_pGlyphMap - > GetGlyph ( 0x2026 , ActualSize ) ; // …
if ( pEllipsisGlyph = = nullptr )
2022-05-31 10:20:45 +00:00
{
// no ellipsis char in font, just stop at end instead
pCursor - > m_Flags & = ~ TEXTFLAG_ELLIPSIS_AT_END ;
pCursor - > m_Flags | = TEXTFLAG_STOP_AT_END ;
}
}
}
2018-03-15 02:33:22 +00:00
2023-02-17 17:07:17 +00:00
const unsigned RenderFlags = TextContainer . m_RenderFlags ;
2018-03-21 14:43:56 +00:00
2023-02-17 17:07:17 +00:00
float DrawX = 0.0f , DrawY = 0.0f ;
2020-09-26 19:41:58 +00:00
if ( ( RenderFlags & TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT ) ! = 0 )
2018-03-21 14:43:56 +00:00
{
DrawX = pCursor - > m_X ;
DrawY = pCursor - > m_Y ;
}
else
{
DrawX = CursorX ;
DrawY = CursorY ;
}
2023-02-17 17:07:17 +00:00
int LineCount = pCursor - > m_LineCount ;
2019-01-06 05:42:57 +00:00
2023-02-17 17:07:17 +00:00
const bool IsRendered = ( pCursor - > m_Flags & TEXTFLAG_RENDER ) ! = 0 ;
2021-09-12 17:40:23 +00:00
2023-02-17 17:07:17 +00:00
const float CursorInnerWidth = ( ( ( ScreenX1 - ScreenX0 ) / Graphics ( ) - > ScreenWidth ( ) ) ) * 2 ;
const float CursorOuterWidth = CursorInnerWidth * 2 ;
const float CursorOuterInnerDiff = ( CursorOuterWidth - CursorInnerWidth ) / 2 ;
2021-09-16 14:50:17 +00:00
2022-06-15 17:34:41 +00:00
std : : vector < IGraphics : : CQuadItem > vSelectionQuads ;
2023-12-27 17:13:10 +00:00
int SelectionQuadLine = - 1 ;
2021-09-12 17:40:23 +00:00
bool SelectionStarted = false ;
bool SelectionUsedPress = false ;
bool SelectionUsedRelease = false ;
int SelectionStartChar = - 1 ;
int SelectionEndChar = - 1 ;
2023-07-17 13:49:41 +00:00
const auto & & CheckInsideChar = [ & ] ( bool CheckOuter , vec2 CursorPos , float LastCharX , float LastCharWidth , float CharX , float CharWidth , float CharY ) - > bool {
Port line input and IME support from 0.7
Port the line input (UI edit boxes, chat, console) and Input Method Editor (IME) support from upstream. Closes #4397.
General
------------------------------
Fix issues with the text input. Closes #4346. Closes #4524.
Word skipping (when holding Ctrl) is overhauled to be consistent with the Windows / Firefox experience that I took as reference.
Improve usability by not blinking (i.e. always rendering) the caret shortly after is has been moved.
UI text input
------------------------------
Fix inconsistent mouse-based left and right scrolling (closes #4347).
Support smooth left and right scrolling.
Chat
------------------------------
Support keyboard-based text selection of the chat input.
Mouse-based selection could be support in the future when we decide to add something like an ingame UI cursor.
Support smooth up and down scrolling of the chat input, removing the old hack that offsets the input string to simulate scrolling.
Console
------------------------------
Also support mouse-based text selection of the command input.
Only text from either the command input or the console log can be selected at the same time. This ensures that Ctrl+C will always copy the text that is currently visually selected in the console.
Check for Ctrl+C input event in event handler instead of in render function, to hopefully fix the issue that copying does not work sometimes (closes #5974 until further notice).
When Ctrl+C is used to copy text from the console log, the selection is cleared. This should make it more clear when text was copied from the log.
Fix an issue that was preventing the console log selection from being cleared, when all log lines are selected.
Remove Ctrl+A/E hotkeys that move cursor to beginning/end respectively. Ctrl+A now selectes all text like for all other inputs. Home and End keys can still be used to go the beginning and end.
Remove Ctrl+U/K hotkeys that clear everything before/after the cursor respectively. Hold shift and use Home/End to select everything instead.
IME support
------------------------------
Render list of IME candidates in the client on Windows, so the candidate list can also be viewed in fullscreen mode. There is no API available to retrieve a candidate list on the other operating systems.
Improve composition rendering by underlining the composition text instead of putting it in square brackets.
Track active input globally to properly activate and deactivate IME through the SDL functions.
Closes #1030. Closes #1008.
Password rendering
------------------------------
Fix rendering of passwords containing unicode. Instead of rendering one star character for each UTF-8 `char`, render on star for every unicode codepoint.
Show the composition text also for passwords. Without seeing the composition text it's hard to type a password containing those characters. The candidate window exposes the composition anyway. If you don't want to expose your password this way, e.g. while streaming, you could:
1. Use a latin password and switch off the IME for the password input with the IME hotkey.
2. Blank your screen with an external program while you are streaming and entering passwords.
3. Use binds to authenticate in rcon or to set the server browser password.
Refactoring
------------------------------
Move all text input logic and general rendering to `CLineInput`.
A `CLineInput` is associated with a particular `char` buffer given as a pointer either in the constructor or with `SetBuffer`. The maximum byte size of the buffer must also be specified. The maximum length in unicode codepoints can also be specified separately (e.g. on upstream, name are limited by the number of unicode codepoints instead).
Add `CLineInputBuffered`, which is a `CLineInput` that own a `char` buffer of a fixed size, which is specified as a template argument. As `CLineInput` does not own a buffer anymore, this reduces duplicate code for line inputs that need their own buffer.
Add `CLineInputNumber` which has additional convenience functions to consider the text as an `int` or `float`, to reduce duplicate code in those cases. In the future we could also add an input filter function so that only numbers can be entered in the number input.
Add `CLineInput::SetClipboardLineCallback` to handle the case that multiple lines of text are pasted into a lineinput. This reduces duplicate code, as this behavior was previously implemented separately for chat and console. The behavior is also fixed to be consistent with the console on Windows, so the first line being pasted edits the current input text and then sends it instead of being sent on its own without the existing input text.
Add `CalcFontSizeAndBoundingBox` to UI to reduce duplicate code. Expose `CalcAlignedCursorPos` as static member function to reuse it for line input.
Dispatch input events to UI inputs through the event handler instead of storing them in a duplicate buffer.
Use `size_t` for line input cursor position, length etc. and for `str_utf8_stats`.
Add `IButtonColorFunction` to UI to describe a functions that defines colors for the Default, Active and Hovered states of UI elements. Add some default button color functions. Use button color function to reduce duplicate code in scrollbar rendering.
Use `vec2` instead of two `floats` to represent the mouse positions in the text renderer.
Remove `CaretPosition` again, as it does not calculate the correct Y position near line breaks due to the wrapping being different when not rendering the entire string. Instead, calculate the exact caret position when rending a text container and store the caret position in the text cursor for later use.
IME usage guide (Windows)
------------------------------
1. Install the respective language and the Microsoft-IME keyboard (e.g. for Chinese, Japanese or Korean).
2. Launch the game (or a text editor to first try out the IME). Note that Windows may track the input language separately for every application. You can change this in the Windows input settings so the input language is changed globally.
2. Switch the input language using the hotkey Windows+Space or another hotkey that you configured in the Windows input settings (Alt+Shift is the default, but you should consider disabling it, to avoid accidentally changing the input language while playing).
3. Switch from Latin/English input mode to the respective asian input mode.
- Chinese: Use Ctrl+Space to switch between English and Chinese input mode. You can change this hotkey in the IME's settings.
- Japanese: Use Ctrl+Space to switch between Alphanumeric and Hiragana/Katakana input mode. You can change this hotkey in the IME's settings.
- Korean: Use Right Alt to switch between English and Hangul input mode. You cannot change this hotkey as of yet.
- Note that the input mode is also tracked per application, but there is no setting to change this behavior as far as I know, so you'll need to switch for every application separately.
4. Start typing. The underlined text is the current composition text. While a composition is active, you can only edit the composition text. Confirm the composition with Space or by selecting a candidate from the candidate list with the arrow keys. Cancel the composition with Escape or by using Backspace to delete the composition text. Note that not all languages offer a candidate list.
SDL version-specific issues
------------------------------
- 2.26.5, 2.24.2, 2.0.22: IME candidates work. But there are minor bugs when moving the composition cursor.
- 2.0.18, 2.0.20: IME candidates work.
- 2.0.16 (our current version): IME candidates cannot be determined with Windows API. Windows tries to draw the composition window like before, so this does not work in fullscreen mode.
- 2.0.8 (upstream 0.7): IME candidates work. But this SDL version is too old for us.
2023-01-03 21:28:38 +00:00
return ( LastCharX - LastCharWidth / 2 < = CursorPos . x & &
CharX + CharWidth / 2 > CursorPos . x & &
2023-12-11 21:14:45 +00:00
CursorPos . y > = CharY - pCursor - > m_AlignedFontSize & &
CursorPos . y < CharY + pCursor - > m_AlignedLineSpacing ) | |
2022-01-22 16:34:23 +00:00
( CheckOuter & &
2023-12-11 21:14:45 +00:00
CursorPos . y < = CharY - pCursor - > m_AlignedFontSize ) ;
2021-09-16 14:50:17 +00:00
} ;
2023-07-17 13:49:41 +00:00
const auto & & CheckSelectionStart = [ & ] ( bool CheckOuter , vec2 CursorPos , int & SelectionChar , bool & SelectionUsedCase , float LastCharX , float LastCharWidth , float CharX , float CharWidth , float CharY ) {
2023-12-11 21:14:45 +00:00
if ( ! SelectionStarted & & ! SelectionUsedCase & &
CheckInsideChar ( CheckOuter , CursorPos , LastCharX , LastCharWidth , CharX , CharWidth , CharY ) )
2021-09-12 17:40:23 +00:00
{
2023-12-11 21:14:45 +00:00
SelectionChar = pCursor - > m_GlyphCount ;
SelectionStarted = ! SelectionStarted ;
SelectionUsedCase = true ;
2021-09-12 17:40:23 +00:00
}
} ;
2023-07-17 13:49:41 +00:00
const auto & & CheckOutsideChar = [ & ] ( bool CheckOuter , vec2 CursorPos , float CharX , float CharWidth , float CharY ) - > bool {
Port line input and IME support from 0.7
Port the line input (UI edit boxes, chat, console) and Input Method Editor (IME) support from upstream. Closes #4397.
General
------------------------------
Fix issues with the text input. Closes #4346. Closes #4524.
Word skipping (when holding Ctrl) is overhauled to be consistent with the Windows / Firefox experience that I took as reference.
Improve usability by not blinking (i.e. always rendering) the caret shortly after is has been moved.
UI text input
------------------------------
Fix inconsistent mouse-based left and right scrolling (closes #4347).
Support smooth left and right scrolling.
Chat
------------------------------
Support keyboard-based text selection of the chat input.
Mouse-based selection could be support in the future when we decide to add something like an ingame UI cursor.
Support smooth up and down scrolling of the chat input, removing the old hack that offsets the input string to simulate scrolling.
Console
------------------------------
Also support mouse-based text selection of the command input.
Only text from either the command input or the console log can be selected at the same time. This ensures that Ctrl+C will always copy the text that is currently visually selected in the console.
Check for Ctrl+C input event in event handler instead of in render function, to hopefully fix the issue that copying does not work sometimes (closes #5974 until further notice).
When Ctrl+C is used to copy text from the console log, the selection is cleared. This should make it more clear when text was copied from the log.
Fix an issue that was preventing the console log selection from being cleared, when all log lines are selected.
Remove Ctrl+A/E hotkeys that move cursor to beginning/end respectively. Ctrl+A now selectes all text like for all other inputs. Home and End keys can still be used to go the beginning and end.
Remove Ctrl+U/K hotkeys that clear everything before/after the cursor respectively. Hold shift and use Home/End to select everything instead.
IME support
------------------------------
Render list of IME candidates in the client on Windows, so the candidate list can also be viewed in fullscreen mode. There is no API available to retrieve a candidate list on the other operating systems.
Improve composition rendering by underlining the composition text instead of putting it in square brackets.
Track active input globally to properly activate and deactivate IME through the SDL functions.
Closes #1030. Closes #1008.
Password rendering
------------------------------
Fix rendering of passwords containing unicode. Instead of rendering one star character for each UTF-8 `char`, render on star for every unicode codepoint.
Show the composition text also for passwords. Without seeing the composition text it's hard to type a password containing those characters. The candidate window exposes the composition anyway. If you don't want to expose your password this way, e.g. while streaming, you could:
1. Use a latin password and switch off the IME for the password input with the IME hotkey.
2. Blank your screen with an external program while you are streaming and entering passwords.
3. Use binds to authenticate in rcon or to set the server browser password.
Refactoring
------------------------------
Move all text input logic and general rendering to `CLineInput`.
A `CLineInput` is associated with a particular `char` buffer given as a pointer either in the constructor or with `SetBuffer`. The maximum byte size of the buffer must also be specified. The maximum length in unicode codepoints can also be specified separately (e.g. on upstream, name are limited by the number of unicode codepoints instead).
Add `CLineInputBuffered`, which is a `CLineInput` that own a `char` buffer of a fixed size, which is specified as a template argument. As `CLineInput` does not own a buffer anymore, this reduces duplicate code for line inputs that need their own buffer.
Add `CLineInputNumber` which has additional convenience functions to consider the text as an `int` or `float`, to reduce duplicate code in those cases. In the future we could also add an input filter function so that only numbers can be entered in the number input.
Add `CLineInput::SetClipboardLineCallback` to handle the case that multiple lines of text are pasted into a lineinput. This reduces duplicate code, as this behavior was previously implemented separately for chat and console. The behavior is also fixed to be consistent with the console on Windows, so the first line being pasted edits the current input text and then sends it instead of being sent on its own without the existing input text.
Add `CalcFontSizeAndBoundingBox` to UI to reduce duplicate code. Expose `CalcAlignedCursorPos` as static member function to reuse it for line input.
Dispatch input events to UI inputs through the event handler instead of storing them in a duplicate buffer.
Use `size_t` for line input cursor position, length etc. and for `str_utf8_stats`.
Add `IButtonColorFunction` to UI to describe a functions that defines colors for the Default, Active and Hovered states of UI elements. Add some default button color functions. Use button color function to reduce duplicate code in scrollbar rendering.
Use `vec2` instead of two `floats` to represent the mouse positions in the text renderer.
Remove `CaretPosition` again, as it does not calculate the correct Y position near line breaks due to the wrapping being different when not rendering the entire string. Instead, calculate the exact caret position when rending a text container and store the caret position in the text cursor for later use.
IME usage guide (Windows)
------------------------------
1. Install the respective language and the Microsoft-IME keyboard (e.g. for Chinese, Japanese or Korean).
2. Launch the game (or a text editor to first try out the IME). Note that Windows may track the input language separately for every application. You can change this in the Windows input settings so the input language is changed globally.
2. Switch the input language using the hotkey Windows+Space or another hotkey that you configured in the Windows input settings (Alt+Shift is the default, but you should consider disabling it, to avoid accidentally changing the input language while playing).
3. Switch from Latin/English input mode to the respective asian input mode.
- Chinese: Use Ctrl+Space to switch between English and Chinese input mode. You can change this hotkey in the IME's settings.
- Japanese: Use Ctrl+Space to switch between Alphanumeric and Hiragana/Katakana input mode. You can change this hotkey in the IME's settings.
- Korean: Use Right Alt to switch between English and Hangul input mode. You cannot change this hotkey as of yet.
- Note that the input mode is also tracked per application, but there is no setting to change this behavior as far as I know, so you'll need to switch for every application separately.
4. Start typing. The underlined text is the current composition text. While a composition is active, you can only edit the composition text. Confirm the composition with Space or by selecting a candidate from the candidate list with the arrow keys. Cancel the composition with Escape or by using Backspace to delete the composition text. Note that not all languages offer a candidate list.
SDL version-specific issues
------------------------------
- 2.26.5, 2.24.2, 2.0.22: IME candidates work. But there are minor bugs when moving the composition cursor.
- 2.0.18, 2.0.20: IME candidates work.
- 2.0.16 (our current version): IME candidates cannot be determined with Windows API. Windows tries to draw the composition window like before, so this does not work in fullscreen mode.
- 2.0.8 (upstream 0.7): IME candidates work. But this SDL version is too old for us.
2023-01-03 21:28:38 +00:00
return ( CharX + CharWidth / 2 > CursorPos . x & &
2023-12-11 21:14:45 +00:00
CursorPos . y > = CharY - pCursor - > m_AlignedFontSize & &
CursorPos . y < CharY + pCursor - > m_AlignedLineSpacing ) | |
2022-01-22 16:34:23 +00:00
( CheckOuter & &
2023-12-11 21:14:45 +00:00
CursorPos . y > = CharY + pCursor - > m_AlignedLineSpacing ) ;
2021-09-16 14:50:17 +00:00
} ;
2023-07-17 13:49:41 +00:00
const auto & & CheckSelectionEnd = [ & ] ( bool CheckOuter , vec2 CursorPos , int & SelectionChar , bool & SelectionUsedCase , float CharX , float CharWidth , float CharY ) {
2023-12-11 21:14:45 +00:00
if ( SelectionStarted & & ! SelectionUsedCase & &
CheckOutsideChar ( CheckOuter , CursorPos , CharX , CharWidth , CharY ) )
2021-09-12 17:40:23 +00:00
{
2023-12-11 21:14:45 +00:00
SelectionChar = pCursor - > m_GlyphCount ;
SelectionStarted = ! SelectionStarted ;
SelectionUsedCase = true ;
2021-09-12 17:40:23 +00:00
}
} ;
float LastSelX = DrawX ;
float LastSelWidth = 0 ;
float LastCharX = DrawX ;
float LastCharWidth = 0 ;
2023-09-06 16:04:14 +00:00
// Returns true if line was started
2023-07-17 13:49:41 +00:00
const auto & & StartNewLine = [ & ] ( ) {
2023-09-06 16:04:14 +00:00
if ( pCursor - > m_MaxLines > 0 & & LineCount > = pCursor - > m_MaxLines )
return false ;
2021-09-12 17:40:23 +00:00
DrawX = pCursor - > m_StartX ;
2023-12-11 21:14:45 +00:00
DrawY + = pCursor - > m_AlignedFontSize + pCursor - > m_AlignedLineSpacing ;
2021-09-12 17:40:23 +00:00
if ( ( RenderFlags & TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT ) = = 0 )
{
2023-07-17 13:49:41 +00:00
DrawX = round_to_int ( DrawX * FakeToScreen . x ) / FakeToScreen . x ; // realign
DrawY = round_to_int ( DrawY * FakeToScreen . y ) / FakeToScreen . y ;
2021-09-12 17:40:23 +00:00
}
LastSelX = DrawX ;
LastSelWidth = 0 ;
LastCharX = DrawX ;
LastCharWidth = 0 ;
+ + LineCount ;
2023-09-06 16:04:14 +00:00
return true ;
2021-09-12 17:40:23 +00:00
} ;
2021-09-16 14:50:17 +00:00
if ( pCursor - > m_CalculateSelectionMode ! = TEXT_CURSOR_SELECTION_MODE_NONE | | pCursor - > m_CursorMode ! = TEXT_CURSOR_CURSOR_MODE_NONE )
2021-09-12 17:40:23 +00:00
{
if ( IsRendered )
2023-02-14 23:39:47 +00:00
Graphics ( ) - > QuadContainerReset ( TextContainer . m_StringInfo . m_SelectionQuadContainerIndex ) ;
2021-09-16 14:50:17 +00:00
// if in calculate mode, also calculate the cursor
if ( pCursor - > m_CursorMode = = TEXT_CURSOR_CURSOR_MODE_CALCULATE )
pCursor - > m_CursorCharacter = - 1 ;
2021-09-12 17:40:23 +00:00
}
2023-02-17 17:07:17 +00:00
IGraphics : : CQuadItem aCursorQuads [ 2 ] ;
bool HasCursor = false ;
2023-07-17 13:49:41 +00:00
const SGlyph * pLastGlyph = nullptr ;
2023-02-17 17:07:17 +00:00
bool GotNewLineLast = false ;
2023-12-02 18:29:31 +00:00
int ColorOption = 0 ;
2023-09-06 16:04:14 +00:00
while ( pCurrent < pEnd & & pCurrent ! = pEllipsis )
2018-03-13 20:49:07 +00:00
{
2023-02-17 17:07:17 +00:00
bool NewLine = false ;
2018-03-13 20:49:07 +00:00
const char * pBatchEnd = pEnd ;
2022-05-31 10:20:45 +00:00
if ( pCursor - > m_LineWidth > 0 & & ! ( pCursor - > m_Flags & TEXTFLAG_STOP_AT_END ) & & ! ( pCursor - > m_Flags & TEXTFLAG_ELLIPSIS_AT_END ) )
2018-03-13 20:49:07 +00:00
{
2023-02-17 17:07:17 +00:00
int Wlen = minimum ( WordLength ( pCurrent ) , ( int ) ( pEnd - pCurrent ) ) ;
2018-03-13 20:49:07 +00:00
CTextCursor Compare = * pCursor ;
2021-09-12 17:40:23 +00:00
Compare . m_CalculateSelectionMode = TEXT_CURSOR_SELECTION_MODE_NONE ;
2021-09-16 14:50:17 +00:00
Compare . m_CursorMode = TEXT_CURSOR_CURSOR_MODE_NONE ;
2018-03-13 20:49:07 +00:00
Compare . m_X = DrawX ;
Compare . m_Y = DrawY ;
Compare . m_Flags & = ~ TEXTFLAG_RENDER ;
2023-02-18 13:00:18 +00:00
Compare . m_Flags | = TEXTFLAG_DISALLOW_NEWLINE ;
2018-03-13 20:49:07 +00:00
Compare . m_LineWidth = - 1 ;
TextEx ( & Compare , pCurrent , Wlen ) ;
if ( Compare . m_X - DrawX > pCursor - > m_LineWidth )
{
// word can't be fitted in one line, cut it
CTextCursor Cutter = * pCursor ;
2021-09-12 17:40:23 +00:00
Cutter . m_CalculateSelectionMode = TEXT_CURSOR_SELECTION_MODE_NONE ;
2021-09-16 14:50:17 +00:00
Cutter . m_CursorMode = TEXT_CURSOR_CURSOR_MODE_NONE ;
2020-09-03 22:53:26 +00:00
Cutter . m_GlyphCount = 0 ;
2018-03-13 20:49:07 +00:00
Cutter . m_CharCount = 0 ;
Cutter . m_X = DrawX ;
Cutter . m_Y = DrawY ;
Cutter . m_Flags & = ~ TEXTFLAG_RENDER ;
2023-02-18 13:00:18 +00:00
Cutter . m_Flags | = TEXTFLAG_STOP_AT_END | TEXTFLAG_DISALLOW_NEWLINE ;
2018-03-13 20:49:07 +00:00
2019-04-11 10:21:42 +00:00
TextEx ( & Cutter , pCurrent , Wlen ) ;
2023-07-10 15:44:49 +00:00
Wlen = str_utf8_rewind ( pCurrent , Cutter . m_CharCount ) ; // rewind once to skip the last character that did not fit
2023-02-17 17:07:17 +00:00
NewLine = true ;
2018-03-13 20:49:07 +00:00
2023-02-17 17:07:17 +00:00
if ( Cutter . m_GlyphCount < = 3 & & ! GotNewLineLast ) // if we can't place 3 chars of the word on this line, take the next
2018-03-13 20:49:07 +00:00
Wlen = 0 ;
}
2023-02-17 17:07:17 +00:00
else if ( Compare . m_X - pCursor - > m_StartX > pCursor - > m_LineWidth & & ! GotNewLineLast )
2018-03-13 20:49:07 +00:00
{
2023-02-17 17:07:17 +00:00
NewLine = true ;
2018-03-13 20:49:07 +00:00
Wlen = 0 ;
}
pBatchEnd = pCurrent + Wlen ;
}
const char * pTmp = pCurrent ;
int NextCharacter = str_utf8_decode ( & pTmp ) ;
2020-10-13 20:08:52 +00:00
2022-05-31 10:20:45 +00:00
while ( pCurrent < pBatchEnd & & pCurrent ! = pEllipsis )
2018-03-13 20:49:07 +00:00
{
2024-06-05 16:01:42 +00:00
const int PrevCharCount = pCursor - > m_CharCount ;
2021-09-12 17:40:23 +00:00
pCursor - > m_CharCount + = pTmp - pCurrent ;
2022-05-22 21:04:32 +00:00
pCurrent = pTmp ;
2023-02-18 13:00:18 +00:00
int Character = NextCharacter ;
2018-03-13 20:49:07 +00:00
NextCharacter = str_utf8_decode ( & pTmp ) ;
if ( Character = = ' \n ' )
{
2023-02-18 13:00:18 +00:00
if ( ( pCursor - > m_Flags & TEXTFLAG_DISALLOW_NEWLINE ) = = 0 )
{
2023-07-17 13:49:41 +00:00
pLastGlyph = nullptr ;
2023-09-06 16:04:14 +00:00
if ( ! StartNewLine ( ) )
2023-02-18 13:00:18 +00:00
break ;
continue ;
}
else
{
Character = ' ' ;
}
2018-03-13 20:49:07 +00:00
}
2023-07-17 13:49:41 +00:00
const SGlyph * pGlyph = m_pGlyphMap - > GetGlyph ( Character , ActualSize ) ;
if ( pGlyph )
2018-03-13 20:49:07 +00:00
{
2023-07-17 13:49:41 +00:00
const float Scale = 1.0f / pGlyph - > m_FontSize ;
const bool ApplyBearingX = ! ( ( ( RenderFlags & TEXT_RENDER_FLAG_NO_X_BEARING ) ! = 0 ) | | ( pCursor - > m_GlyphCount = = 0 & & ( RenderFlags & TEXT_RENDER_FLAG_NO_FIRST_CHARACTER_X_BEARING ) ! = 0 ) ) ;
const float Advance = ( ( ( ( RenderFlags & TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH ) ! = 0 ) ? ( pGlyph - > m_Width ) : ( pGlyph - > m_AdvanceX + ( ( ! ApplyBearingX ) ? ( - pGlyph - > m_OffsetX ) : 0.f ) ) ) ) * Scale * pCursor - > m_AlignedFontSize ;
2021-09-12 17:40:23 +00:00
2023-07-17 13:49:41 +00:00
const float OutLineRealDiff = ( pGlyph - > m_Width - pGlyph - > m_CharWidth ) * Scale * pCursor - > m_AlignedFontSize ;
2019-04-26 19:36:49 +00:00
2023-07-17 13:49:41 +00:00
float CharKerning = 0.0f ;
2020-09-26 19:41:58 +00:00
if ( ( RenderFlags & TEXT_RENDER_FLAG_KERNING ) ! = 0 )
2023-07-17 13:49:41 +00:00
CharKerning = m_pGlyphMap - > Kerning ( pLastGlyph , pGlyph ) . x * Scale * pCursor - > m_AlignedFontSize ;
pLastGlyph = pGlyph ;
2019-01-06 05:42:57 +00:00
2023-07-17 13:49:41 +00:00
if ( pEllipsisGlyph ! = nullptr & & pCursor - > m_Flags & TEXTFLAG_ELLIPSIS_AT_END & & pCurrent < pBatchEnd & & pCurrent ! = pEllipsis )
2022-05-31 10:20:45 +00:00
{
2023-07-17 13:49:41 +00:00
float AdvanceEllipsis = ( ( ( ( RenderFlags & TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH ) ! = 0 ) ? ( pEllipsisGlyph - > m_Width ) : ( pEllipsisGlyph - > m_AdvanceX + ( ( ! ApplyBearingX ) ? ( - pEllipsisGlyph - > m_OffsetX ) : 0.f ) ) ) ) * Scale * pCursor - > m_AlignedFontSize ;
float CharKerningEllipsis = 0.0f ;
2022-05-31 10:20:45 +00:00
if ( ( RenderFlags & TEXT_RENDER_FLAG_KERNING ) ! = 0 )
{
2023-07-17 13:49:41 +00:00
CharKerningEllipsis = m_pGlyphMap - > Kerning ( pGlyph , pEllipsisGlyph ) . x * Scale * pCursor - > m_AlignedFontSize ;
2022-05-31 10:20:45 +00:00
}
if ( DrawX + CharKerning + Advance + CharKerningEllipsis + AdvanceEllipsis - pCursor - > m_StartX > pCursor - > m_LineWidth )
{
// we hit the end, only render ellipsis and finish
pTmp = pEllipsis ;
NextCharacter = 0x2026 ;
continue ;
}
}
2022-05-22 21:04:32 +00:00
if ( pCursor - > m_Flags & TEXTFLAG_STOP_AT_END & & ( DrawX + CharKerning ) + Advance - pCursor - > m_StartX > pCursor - > m_LineWidth )
2018-03-13 20:49:07 +00:00
{
// we hit the end of the line, no more to render or count
pCurrent = pEnd ;
break ;
}
2023-07-17 13:49:41 +00:00
float BearingX = ( ! ApplyBearingX ? 0.f : pGlyph - > m_OffsetX ) * Scale * pCursor - > m_AlignedFontSize ;
float CharWidth = pGlyph - > m_Width * Scale * pCursor - > m_AlignedFontSize ;
2019-04-26 19:36:49 +00:00
2023-07-17 13:49:41 +00:00
float BearingY = ( ( ( RenderFlags & TEXT_RENDER_FLAG_NO_Y_BEARING ) ! = 0 ) ? 0.f : ( pGlyph - > m_OffsetY * Scale * pCursor - > m_AlignedFontSize ) ) ;
float CharHeight = pGlyph - > m_Height * Scale * pCursor - > m_AlignedFontSize ;
2020-10-06 10:25:10 +00:00
if ( ( RenderFlags & TEXT_RENDER_FLAG_NO_OVERSIZE ) ! = 0 )
{
2023-07-17 13:49:41 +00:00
if ( CharHeight + BearingY > pCursor - > m_AlignedFontSize )
2020-10-06 10:25:10 +00:00
{
BearingY = 0 ;
2023-07-17 13:49:41 +00:00
float ScaleChar = ( CharHeight + BearingY ) / pCursor - > m_AlignedFontSize ;
CharHeight = pCursor - > m_AlignedFontSize ;
2020-10-06 10:25:10 +00:00
CharWidth / = ScaleChar ;
}
}
2023-07-17 13:49:41 +00:00
const float TmpY = ( DrawY + pCursor - > m_AlignedFontSize ) ;
const float CharX = ( DrawX + CharKerning ) + BearingX ;
const float CharY = TmpY - BearingY ;
2021-09-12 17:40:23 +00:00
2023-12-02 18:29:31 +00:00
// Check if we have any color split
ColorRGBA Color = m_Color ;
if ( ColorOption < ( int ) pCursor - > m_vColorSplits . size ( ) )
{
STextColorSplit & Split = pCursor - > m_vColorSplits . at ( ColorOption ) ;
2023-12-18 01:08:12 +00:00
if ( PrevCharCount > = Split . m_CharIndex & & ( Split . m_Length = = - 1 | | PrevCharCount < Split . m_CharIndex + Split . m_Length ) )
2023-12-02 18:29:31 +00:00
Color = Split . m_Color ;
2023-12-18 01:08:12 +00:00
if ( Split . m_Length ! = - 1 & & PrevCharCount > = ( Split . m_CharIndex + Split . m_Length - 1 ) )
2023-12-31 00:51:07 +00:00
{
2023-12-02 18:29:31 +00:00
ColorOption + + ;
2023-12-31 00:51:07 +00:00
if ( ColorOption < ( int ) pCursor - > m_vColorSplits . size ( ) )
{ // Handle splits that are
Split = pCursor - > m_vColorSplits . at ( ColorOption ) ;
if ( PrevCharCount > = Split . m_CharIndex )
Color = Split . m_Color ;
}
}
2023-12-02 18:29:31 +00:00
}
2018-07-10 09:29:02 +00:00
// don't add text that isn't drawn, the color overwrite is used for that
2023-12-02 18:29:31 +00:00
if ( Color . a ! = 0.f & & IsRendered )
2018-03-13 20:49:07 +00:00
{
2022-06-11 19:38:18 +00:00
TextContainer . m_StringInfo . m_vCharacterQuads . emplace_back ( ) ;
STextCharQuad & TextCharQuad = TextContainer . m_StringInfo . m_vCharacterQuads . back ( ) ;
2018-03-13 20:49:07 +00:00
2022-06-30 22:36:32 +00:00
TextCharQuad . m_aVertices [ 0 ] . m_X = CharX ;
TextCharQuad . m_aVertices [ 0 ] . m_Y = CharY ;
2023-07-17 13:49:41 +00:00
TextCharQuad . m_aVertices [ 0 ] . m_U = pGlyph - > m_aUVs [ 0 ] ;
TextCharQuad . m_aVertices [ 0 ] . m_V = pGlyph - > m_aUVs [ 3 ] ;
2023-12-02 18:29:31 +00:00
TextCharQuad . m_aVertices [ 0 ] . m_Color . r = ( unsigned char ) ( Color . r * 255.f ) ;
TextCharQuad . m_aVertices [ 0 ] . m_Color . g = ( unsigned char ) ( Color . g * 255.f ) ;
TextCharQuad . m_aVertices [ 0 ] . m_Color . b = ( unsigned char ) ( Color . b * 255.f ) ;
TextCharQuad . m_aVertices [ 0 ] . m_Color . a = ( unsigned char ) ( Color . a * 255.f ) ;
2022-06-30 22:36:32 +00:00
TextCharQuad . m_aVertices [ 1 ] . m_X = CharX + CharWidth ;
TextCharQuad . m_aVertices [ 1 ] . m_Y = CharY ;
2023-07-17 13:49:41 +00:00
TextCharQuad . m_aVertices [ 1 ] . m_U = pGlyph - > m_aUVs [ 2 ] ;
TextCharQuad . m_aVertices [ 1 ] . m_V = pGlyph - > m_aUVs [ 3 ] ;
2023-12-02 18:29:31 +00:00
TextCharQuad . m_aVertices [ 1 ] . m_Color . r = ( unsigned char ) ( Color . r * 255.f ) ;
TextCharQuad . m_aVertices [ 1 ] . m_Color . g = ( unsigned char ) ( Color . g * 255.f ) ;
TextCharQuad . m_aVertices [ 1 ] . m_Color . b = ( unsigned char ) ( Color . b * 255.f ) ;
TextCharQuad . m_aVertices [ 1 ] . m_Color . a = ( unsigned char ) ( Color . a * 255.f ) ;
2022-06-30 22:36:32 +00:00
TextCharQuad . m_aVertices [ 2 ] . m_X = CharX + CharWidth ;
TextCharQuad . m_aVertices [ 2 ] . m_Y = CharY - CharHeight ;
2023-07-17 13:49:41 +00:00
TextCharQuad . m_aVertices [ 2 ] . m_U = pGlyph - > m_aUVs [ 2 ] ;
TextCharQuad . m_aVertices [ 2 ] . m_V = pGlyph - > m_aUVs [ 1 ] ;
2023-12-02 18:29:31 +00:00
TextCharQuad . m_aVertices [ 2 ] . m_Color . r = ( unsigned char ) ( Color . r * 255.f ) ;
TextCharQuad . m_aVertices [ 2 ] . m_Color . g = ( unsigned char ) ( Color . g * 255.f ) ;
TextCharQuad . m_aVertices [ 2 ] . m_Color . b = ( unsigned char ) ( Color . b * 255.f ) ;
TextCharQuad . m_aVertices [ 2 ] . m_Color . a = ( unsigned char ) ( Color . a * 255.f ) ;
2022-06-30 22:36:32 +00:00
TextCharQuad . m_aVertices [ 3 ] . m_X = CharX ;
TextCharQuad . m_aVertices [ 3 ] . m_Y = CharY - CharHeight ;
2023-07-17 13:49:41 +00:00
TextCharQuad . m_aVertices [ 3 ] . m_U = pGlyph - > m_aUVs [ 0 ] ;
TextCharQuad . m_aVertices [ 3 ] . m_V = pGlyph - > m_aUVs [ 1 ] ;
2023-12-02 18:29:31 +00:00
TextCharQuad . m_aVertices [ 3 ] . m_Color . r = ( unsigned char ) ( Color . r * 255.f ) ;
TextCharQuad . m_aVertices [ 3 ] . m_Color . g = ( unsigned char ) ( Color . g * 255.f ) ;
TextCharQuad . m_aVertices [ 3 ] . m_Color . b = ( unsigned char ) ( Color . b * 255.f ) ;
TextCharQuad . m_aVertices [ 3 ] . m_Color . a = ( unsigned char ) ( Color . a * 255.f ) ;
2018-03-13 20:49:07 +00:00
}
2021-09-12 17:40:23 +00:00
// calculate the full width from the last selection point to the end of this selection draw on screen
2023-07-17 13:49:41 +00:00
const float SelWidth = ( CharX + maximum ( Advance , CharWidth - OutLineRealDiff / 2 ) ) - ( LastSelX + LastSelWidth ) ;
const float SelX = ( LastSelX + LastSelWidth ) ;
2021-09-12 17:40:23 +00:00
2021-09-16 14:50:17 +00:00
if ( pCursor - > m_CursorMode = = TEXT_CURSOR_CURSOR_MODE_CALCULATE )
{
Port line input and IME support from 0.7
Port the line input (UI edit boxes, chat, console) and Input Method Editor (IME) support from upstream. Closes #4397.
General
------------------------------
Fix issues with the text input. Closes #4346. Closes #4524.
Word skipping (when holding Ctrl) is overhauled to be consistent with the Windows / Firefox experience that I took as reference.
Improve usability by not blinking (i.e. always rendering) the caret shortly after is has been moved.
UI text input
------------------------------
Fix inconsistent mouse-based left and right scrolling (closes #4347).
Support smooth left and right scrolling.
Chat
------------------------------
Support keyboard-based text selection of the chat input.
Mouse-based selection could be support in the future when we decide to add something like an ingame UI cursor.
Support smooth up and down scrolling of the chat input, removing the old hack that offsets the input string to simulate scrolling.
Console
------------------------------
Also support mouse-based text selection of the command input.
Only text from either the command input or the console log can be selected at the same time. This ensures that Ctrl+C will always copy the text that is currently visually selected in the console.
Check for Ctrl+C input event in event handler instead of in render function, to hopefully fix the issue that copying does not work sometimes (closes #5974 until further notice).
When Ctrl+C is used to copy text from the console log, the selection is cleared. This should make it more clear when text was copied from the log.
Fix an issue that was preventing the console log selection from being cleared, when all log lines are selected.
Remove Ctrl+A/E hotkeys that move cursor to beginning/end respectively. Ctrl+A now selectes all text like for all other inputs. Home and End keys can still be used to go the beginning and end.
Remove Ctrl+U/K hotkeys that clear everything before/after the cursor respectively. Hold shift and use Home/End to select everything instead.
IME support
------------------------------
Render list of IME candidates in the client on Windows, so the candidate list can also be viewed in fullscreen mode. There is no API available to retrieve a candidate list on the other operating systems.
Improve composition rendering by underlining the composition text instead of putting it in square brackets.
Track active input globally to properly activate and deactivate IME through the SDL functions.
Closes #1030. Closes #1008.
Password rendering
------------------------------
Fix rendering of passwords containing unicode. Instead of rendering one star character for each UTF-8 `char`, render on star for every unicode codepoint.
Show the composition text also for passwords. Without seeing the composition text it's hard to type a password containing those characters. The candidate window exposes the composition anyway. If you don't want to expose your password this way, e.g. while streaming, you could:
1. Use a latin password and switch off the IME for the password input with the IME hotkey.
2. Blank your screen with an external program while you are streaming and entering passwords.
3. Use binds to authenticate in rcon or to set the server browser password.
Refactoring
------------------------------
Move all text input logic and general rendering to `CLineInput`.
A `CLineInput` is associated with a particular `char` buffer given as a pointer either in the constructor or with `SetBuffer`. The maximum byte size of the buffer must also be specified. The maximum length in unicode codepoints can also be specified separately (e.g. on upstream, name are limited by the number of unicode codepoints instead).
Add `CLineInputBuffered`, which is a `CLineInput` that own a `char` buffer of a fixed size, which is specified as a template argument. As `CLineInput` does not own a buffer anymore, this reduces duplicate code for line inputs that need their own buffer.
Add `CLineInputNumber` which has additional convenience functions to consider the text as an `int` or `float`, to reduce duplicate code in those cases. In the future we could also add an input filter function so that only numbers can be entered in the number input.
Add `CLineInput::SetClipboardLineCallback` to handle the case that multiple lines of text are pasted into a lineinput. This reduces duplicate code, as this behavior was previously implemented separately for chat and console. The behavior is also fixed to be consistent with the console on Windows, so the first line being pasted edits the current input text and then sends it instead of being sent on its own without the existing input text.
Add `CalcFontSizeAndBoundingBox` to UI to reduce duplicate code. Expose `CalcAlignedCursorPos` as static member function to reuse it for line input.
Dispatch input events to UI inputs through the event handler instead of storing them in a duplicate buffer.
Use `size_t` for line input cursor position, length etc. and for `str_utf8_stats`.
Add `IButtonColorFunction` to UI to describe a functions that defines colors for the Default, Active and Hovered states of UI elements. Add some default button color functions. Use button color function to reduce duplicate code in scrollbar rendering.
Use `vec2` instead of two `floats` to represent the mouse positions in the text renderer.
Remove `CaretPosition` again, as it does not calculate the correct Y position near line breaks due to the wrapping being different when not rendering the entire string. Instead, calculate the exact caret position when rending a text container and store the caret position in the text cursor for later use.
IME usage guide (Windows)
------------------------------
1. Install the respective language and the Microsoft-IME keyboard (e.g. for Chinese, Japanese or Korean).
2. Launch the game (or a text editor to first try out the IME). Note that Windows may track the input language separately for every application. You can change this in the Windows input settings so the input language is changed globally.
2. Switch the input language using the hotkey Windows+Space or another hotkey that you configured in the Windows input settings (Alt+Shift is the default, but you should consider disabling it, to avoid accidentally changing the input language while playing).
3. Switch from Latin/English input mode to the respective asian input mode.
- Chinese: Use Ctrl+Space to switch between English and Chinese input mode. You can change this hotkey in the IME's settings.
- Japanese: Use Ctrl+Space to switch between Alphanumeric and Hiragana/Katakana input mode. You can change this hotkey in the IME's settings.
- Korean: Use Right Alt to switch between English and Hangul input mode. You cannot change this hotkey as of yet.
- Note that the input mode is also tracked per application, but there is no setting to change this behavior as far as I know, so you'll need to switch for every application separately.
4. Start typing. The underlined text is the current composition text. While a composition is active, you can only edit the composition text. Confirm the composition with Space or by selecting a candidate from the candidate list with the arrow keys. Cancel the composition with Escape or by using Backspace to delete the composition text. Note that not all languages offer a candidate list.
SDL version-specific issues
------------------------------
- 2.26.5, 2.24.2, 2.0.22: IME candidates work. But there are minor bugs when moving the composition cursor.
- 2.0.18, 2.0.20: IME candidates work.
- 2.0.16 (our current version): IME candidates cannot be determined with Windows API. Windows tries to draw the composition window like before, so this does not work in fullscreen mode.
- 2.0.8 (upstream 0.7): IME candidates work. But this SDL version is too old for us.
2023-01-03 21:28:38 +00:00
if ( pCursor - > m_CursorCharacter = = - 1 & & CheckInsideChar ( pCursor - > m_GlyphCount = = 0 , pCursor - > m_ReleaseMouse , pCursor - > m_GlyphCount = = 0 ? std : : numeric_limits < float > : : lowest ( ) : LastCharX , LastCharWidth , CharX , CharWidth , TmpY ) )
2021-09-16 14:50:17 +00:00
{
2023-04-13 21:59:06 +00:00
pCursor - > m_CursorCharacter = pCursor - > m_GlyphCount ;
2021-09-16 14:50:17 +00:00
}
}
2021-09-12 17:40:23 +00:00
if ( pCursor - > m_CalculateSelectionMode = = TEXT_CURSOR_SELECTION_MODE_CALCULATE )
{
2023-04-13 21:59:06 +00:00
if ( pCursor - > m_GlyphCount = = 0 )
2021-09-12 17:40:23 +00:00
{
Port line input and IME support from 0.7
Port the line input (UI edit boxes, chat, console) and Input Method Editor (IME) support from upstream. Closes #4397.
General
------------------------------
Fix issues with the text input. Closes #4346. Closes #4524.
Word skipping (when holding Ctrl) is overhauled to be consistent with the Windows / Firefox experience that I took as reference.
Improve usability by not blinking (i.e. always rendering) the caret shortly after is has been moved.
UI text input
------------------------------
Fix inconsistent mouse-based left and right scrolling (closes #4347).
Support smooth left and right scrolling.
Chat
------------------------------
Support keyboard-based text selection of the chat input.
Mouse-based selection could be support in the future when we decide to add something like an ingame UI cursor.
Support smooth up and down scrolling of the chat input, removing the old hack that offsets the input string to simulate scrolling.
Console
------------------------------
Also support mouse-based text selection of the command input.
Only text from either the command input or the console log can be selected at the same time. This ensures that Ctrl+C will always copy the text that is currently visually selected in the console.
Check for Ctrl+C input event in event handler instead of in render function, to hopefully fix the issue that copying does not work sometimes (closes #5974 until further notice).
When Ctrl+C is used to copy text from the console log, the selection is cleared. This should make it more clear when text was copied from the log.
Fix an issue that was preventing the console log selection from being cleared, when all log lines are selected.
Remove Ctrl+A/E hotkeys that move cursor to beginning/end respectively. Ctrl+A now selectes all text like for all other inputs. Home and End keys can still be used to go the beginning and end.
Remove Ctrl+U/K hotkeys that clear everything before/after the cursor respectively. Hold shift and use Home/End to select everything instead.
IME support
------------------------------
Render list of IME candidates in the client on Windows, so the candidate list can also be viewed in fullscreen mode. There is no API available to retrieve a candidate list on the other operating systems.
Improve composition rendering by underlining the composition text instead of putting it in square brackets.
Track active input globally to properly activate and deactivate IME through the SDL functions.
Closes #1030. Closes #1008.
Password rendering
------------------------------
Fix rendering of passwords containing unicode. Instead of rendering one star character for each UTF-8 `char`, render on star for every unicode codepoint.
Show the composition text also for passwords. Without seeing the composition text it's hard to type a password containing those characters. The candidate window exposes the composition anyway. If you don't want to expose your password this way, e.g. while streaming, you could:
1. Use a latin password and switch off the IME for the password input with the IME hotkey.
2. Blank your screen with an external program while you are streaming and entering passwords.
3. Use binds to authenticate in rcon or to set the server browser password.
Refactoring
------------------------------
Move all text input logic and general rendering to `CLineInput`.
A `CLineInput` is associated with a particular `char` buffer given as a pointer either in the constructor or with `SetBuffer`. The maximum byte size of the buffer must also be specified. The maximum length in unicode codepoints can also be specified separately (e.g. on upstream, name are limited by the number of unicode codepoints instead).
Add `CLineInputBuffered`, which is a `CLineInput` that own a `char` buffer of a fixed size, which is specified as a template argument. As `CLineInput` does not own a buffer anymore, this reduces duplicate code for line inputs that need their own buffer.
Add `CLineInputNumber` which has additional convenience functions to consider the text as an `int` or `float`, to reduce duplicate code in those cases. In the future we could also add an input filter function so that only numbers can be entered in the number input.
Add `CLineInput::SetClipboardLineCallback` to handle the case that multiple lines of text are pasted into a lineinput. This reduces duplicate code, as this behavior was previously implemented separately for chat and console. The behavior is also fixed to be consistent with the console on Windows, so the first line being pasted edits the current input text and then sends it instead of being sent on its own without the existing input text.
Add `CalcFontSizeAndBoundingBox` to UI to reduce duplicate code. Expose `CalcAlignedCursorPos` as static member function to reuse it for line input.
Dispatch input events to UI inputs through the event handler instead of storing them in a duplicate buffer.
Use `size_t` for line input cursor position, length etc. and for `str_utf8_stats`.
Add `IButtonColorFunction` to UI to describe a functions that defines colors for the Default, Active and Hovered states of UI elements. Add some default button color functions. Use button color function to reduce duplicate code in scrollbar rendering.
Use `vec2` instead of two `floats` to represent the mouse positions in the text renderer.
Remove `CaretPosition` again, as it does not calculate the correct Y position near line breaks due to the wrapping being different when not rendering the entire string. Instead, calculate the exact caret position when rending a text container and store the caret position in the text cursor for later use.
IME usage guide (Windows)
------------------------------
1. Install the respective language and the Microsoft-IME keyboard (e.g. for Chinese, Japanese or Korean).
2. Launch the game (or a text editor to first try out the IME). Note that Windows may track the input language separately for every application. You can change this in the Windows input settings so the input language is changed globally.
2. Switch the input language using the hotkey Windows+Space or another hotkey that you configured in the Windows input settings (Alt+Shift is the default, but you should consider disabling it, to avoid accidentally changing the input language while playing).
3. Switch from Latin/English input mode to the respective asian input mode.
- Chinese: Use Ctrl+Space to switch between English and Chinese input mode. You can change this hotkey in the IME's settings.
- Japanese: Use Ctrl+Space to switch between Alphanumeric and Hiragana/Katakana input mode. You can change this hotkey in the IME's settings.
- Korean: Use Right Alt to switch between English and Hangul input mode. You cannot change this hotkey as of yet.
- Note that the input mode is also tracked per application, but there is no setting to change this behavior as far as I know, so you'll need to switch for every application separately.
4. Start typing. The underlined text is the current composition text. While a composition is active, you can only edit the composition text. Confirm the composition with Space or by selecting a candidate from the candidate list with the arrow keys. Cancel the composition with Escape or by using Backspace to delete the composition text. Note that not all languages offer a candidate list.
SDL version-specific issues
------------------------------
- 2.26.5, 2.24.2, 2.0.22: IME candidates work. But there are minor bugs when moving the composition cursor.
- 2.0.18, 2.0.20: IME candidates work.
- 2.0.16 (our current version): IME candidates cannot be determined with Windows API. Windows tries to draw the composition window like before, so this does not work in fullscreen mode.
- 2.0.8 (upstream 0.7): IME candidates work. But this SDL version is too old for us.
2023-01-03 21:28:38 +00:00
CheckSelectionStart ( true , pCursor - > m_PressMouse , SelectionStartChar , SelectionUsedPress , std : : numeric_limits < float > : : lowest ( ) , 0 , CharX , CharWidth , TmpY ) ;
CheckSelectionStart ( true , pCursor - > m_ReleaseMouse , SelectionEndChar , SelectionUsedRelease , std : : numeric_limits < float > : : lowest ( ) , 0 , CharX , CharWidth , TmpY ) ;
2021-09-12 17:40:23 +00:00
}
2022-10-25 16:51:56 +00:00
// if selection didn't start and the mouse pos is at least on 50% of the right side of the character start
Port line input and IME support from 0.7
Port the line input (UI edit boxes, chat, console) and Input Method Editor (IME) support from upstream. Closes #4397.
General
------------------------------
Fix issues with the text input. Closes #4346. Closes #4524.
Word skipping (when holding Ctrl) is overhauled to be consistent with the Windows / Firefox experience that I took as reference.
Improve usability by not blinking (i.e. always rendering) the caret shortly after is has been moved.
UI text input
------------------------------
Fix inconsistent mouse-based left and right scrolling (closes #4347).
Support smooth left and right scrolling.
Chat
------------------------------
Support keyboard-based text selection of the chat input.
Mouse-based selection could be support in the future when we decide to add something like an ingame UI cursor.
Support smooth up and down scrolling of the chat input, removing the old hack that offsets the input string to simulate scrolling.
Console
------------------------------
Also support mouse-based text selection of the command input.
Only text from either the command input or the console log can be selected at the same time. This ensures that Ctrl+C will always copy the text that is currently visually selected in the console.
Check for Ctrl+C input event in event handler instead of in render function, to hopefully fix the issue that copying does not work sometimes (closes #5974 until further notice).
When Ctrl+C is used to copy text from the console log, the selection is cleared. This should make it more clear when text was copied from the log.
Fix an issue that was preventing the console log selection from being cleared, when all log lines are selected.
Remove Ctrl+A/E hotkeys that move cursor to beginning/end respectively. Ctrl+A now selectes all text like for all other inputs. Home and End keys can still be used to go the beginning and end.
Remove Ctrl+U/K hotkeys that clear everything before/after the cursor respectively. Hold shift and use Home/End to select everything instead.
IME support
------------------------------
Render list of IME candidates in the client on Windows, so the candidate list can also be viewed in fullscreen mode. There is no API available to retrieve a candidate list on the other operating systems.
Improve composition rendering by underlining the composition text instead of putting it in square brackets.
Track active input globally to properly activate and deactivate IME through the SDL functions.
Closes #1030. Closes #1008.
Password rendering
------------------------------
Fix rendering of passwords containing unicode. Instead of rendering one star character for each UTF-8 `char`, render on star for every unicode codepoint.
Show the composition text also for passwords. Without seeing the composition text it's hard to type a password containing those characters. The candidate window exposes the composition anyway. If you don't want to expose your password this way, e.g. while streaming, you could:
1. Use a latin password and switch off the IME for the password input with the IME hotkey.
2. Blank your screen with an external program while you are streaming and entering passwords.
3. Use binds to authenticate in rcon or to set the server browser password.
Refactoring
------------------------------
Move all text input logic and general rendering to `CLineInput`.
A `CLineInput` is associated with a particular `char` buffer given as a pointer either in the constructor or with `SetBuffer`. The maximum byte size of the buffer must also be specified. The maximum length in unicode codepoints can also be specified separately (e.g. on upstream, name are limited by the number of unicode codepoints instead).
Add `CLineInputBuffered`, which is a `CLineInput` that own a `char` buffer of a fixed size, which is specified as a template argument. As `CLineInput` does not own a buffer anymore, this reduces duplicate code for line inputs that need their own buffer.
Add `CLineInputNumber` which has additional convenience functions to consider the text as an `int` or `float`, to reduce duplicate code in those cases. In the future we could also add an input filter function so that only numbers can be entered in the number input.
Add `CLineInput::SetClipboardLineCallback` to handle the case that multiple lines of text are pasted into a lineinput. This reduces duplicate code, as this behavior was previously implemented separately for chat and console. The behavior is also fixed to be consistent with the console on Windows, so the first line being pasted edits the current input text and then sends it instead of being sent on its own without the existing input text.
Add `CalcFontSizeAndBoundingBox` to UI to reduce duplicate code. Expose `CalcAlignedCursorPos` as static member function to reuse it for line input.
Dispatch input events to UI inputs through the event handler instead of storing them in a duplicate buffer.
Use `size_t` for line input cursor position, length etc. and for `str_utf8_stats`.
Add `IButtonColorFunction` to UI to describe a functions that defines colors for the Default, Active and Hovered states of UI elements. Add some default button color functions. Use button color function to reduce duplicate code in scrollbar rendering.
Use `vec2` instead of two `floats` to represent the mouse positions in the text renderer.
Remove `CaretPosition` again, as it does not calculate the correct Y position near line breaks due to the wrapping being different when not rendering the entire string. Instead, calculate the exact caret position when rending a text container and store the caret position in the text cursor for later use.
IME usage guide (Windows)
------------------------------
1. Install the respective language and the Microsoft-IME keyboard (e.g. for Chinese, Japanese or Korean).
2. Launch the game (or a text editor to first try out the IME). Note that Windows may track the input language separately for every application. You can change this in the Windows input settings so the input language is changed globally.
2. Switch the input language using the hotkey Windows+Space or another hotkey that you configured in the Windows input settings (Alt+Shift is the default, but you should consider disabling it, to avoid accidentally changing the input language while playing).
3. Switch from Latin/English input mode to the respective asian input mode.
- Chinese: Use Ctrl+Space to switch between English and Chinese input mode. You can change this hotkey in the IME's settings.
- Japanese: Use Ctrl+Space to switch between Alphanumeric and Hiragana/Katakana input mode. You can change this hotkey in the IME's settings.
- Korean: Use Right Alt to switch between English and Hangul input mode. You cannot change this hotkey as of yet.
- Note that the input mode is also tracked per application, but there is no setting to change this behavior as far as I know, so you'll need to switch for every application separately.
4. Start typing. The underlined text is the current composition text. While a composition is active, you can only edit the composition text. Confirm the composition with Space or by selecting a candidate from the candidate list with the arrow keys. Cancel the composition with Escape or by using Backspace to delete the composition text. Note that not all languages offer a candidate list.
SDL version-specific issues
------------------------------
- 2.26.5, 2.24.2, 2.0.22: IME candidates work. But there are minor bugs when moving the composition cursor.
- 2.0.18, 2.0.20: IME candidates work.
- 2.0.16 (our current version): IME candidates cannot be determined with Windows API. Windows tries to draw the composition window like before, so this does not work in fullscreen mode.
- 2.0.8 (upstream 0.7): IME candidates work. But this SDL version is too old for us.
2023-01-03 21:28:38 +00:00
CheckSelectionStart ( false , pCursor - > m_PressMouse , SelectionStartChar , SelectionUsedPress , LastCharX , LastCharWidth , CharX , CharWidth , TmpY ) ;
CheckSelectionStart ( false , pCursor - > m_ReleaseMouse , SelectionEndChar , SelectionUsedRelease , LastCharX , LastCharWidth , CharX , CharWidth , TmpY ) ;
CheckSelectionEnd ( false , pCursor - > m_ReleaseMouse , SelectionEndChar , SelectionUsedRelease , CharX , CharWidth , TmpY ) ;
CheckSelectionEnd ( false , pCursor - > m_PressMouse , SelectionStartChar , SelectionUsedPress , CharX , CharWidth , TmpY ) ;
2021-09-12 17:40:23 +00:00
}
if ( pCursor - > m_CalculateSelectionMode = = TEXT_CURSOR_SELECTION_MODE_SET )
{
2024-05-06 09:42:14 +00:00
if ( pCursor - > m_GlyphCount = = pCursor - > m_SelectionStart )
2021-09-12 17:40:23 +00:00
{
SelectionStarted = ! SelectionStarted ;
2023-04-13 21:59:06 +00:00
SelectionStartChar = pCursor - > m_GlyphCount ;
2021-09-12 17:40:23 +00:00
SelectionUsedPress = true ;
}
2024-05-06 09:42:14 +00:00
if ( pCursor - > m_GlyphCount = = pCursor - > m_SelectionEnd )
2021-09-12 17:40:23 +00:00
{
SelectionStarted = ! SelectionStarted ;
2023-04-13 21:59:06 +00:00
SelectionEndChar = pCursor - > m_GlyphCount ;
2021-09-12 17:40:23 +00:00
SelectionUsedRelease = true ;
}
}
2021-09-16 14:50:17 +00:00
if ( pCursor - > m_CursorMode ! = TEXT_CURSOR_CURSOR_MODE_NONE )
{
2024-05-06 09:42:14 +00:00
if ( pCursor - > m_GlyphCount = = pCursor - > m_CursorCharacter )
2021-09-16 14:50:17 +00:00
{
HasCursor = true ;
2023-07-17 13:49:41 +00:00
aCursorQuads [ 0 ] = IGraphics : : CQuadItem ( SelX - CursorOuterInnerDiff , DrawY , CursorOuterWidth , pCursor - > m_AlignedFontSize ) ;
aCursorQuads [ 1 ] = IGraphics : : CQuadItem ( SelX , DrawY + CursorOuterInnerDiff , CursorInnerWidth , pCursor - > m_AlignedFontSize - CursorOuterInnerDiff * 2 ) ;
Port line input and IME support from 0.7
Port the line input (UI edit boxes, chat, console) and Input Method Editor (IME) support from upstream. Closes #4397.
General
------------------------------
Fix issues with the text input. Closes #4346. Closes #4524.
Word skipping (when holding Ctrl) is overhauled to be consistent with the Windows / Firefox experience that I took as reference.
Improve usability by not blinking (i.e. always rendering) the caret shortly after is has been moved.
UI text input
------------------------------
Fix inconsistent mouse-based left and right scrolling (closes #4347).
Support smooth left and right scrolling.
Chat
------------------------------
Support keyboard-based text selection of the chat input.
Mouse-based selection could be support in the future when we decide to add something like an ingame UI cursor.
Support smooth up and down scrolling of the chat input, removing the old hack that offsets the input string to simulate scrolling.
Console
------------------------------
Also support mouse-based text selection of the command input.
Only text from either the command input or the console log can be selected at the same time. This ensures that Ctrl+C will always copy the text that is currently visually selected in the console.
Check for Ctrl+C input event in event handler instead of in render function, to hopefully fix the issue that copying does not work sometimes (closes #5974 until further notice).
When Ctrl+C is used to copy text from the console log, the selection is cleared. This should make it more clear when text was copied from the log.
Fix an issue that was preventing the console log selection from being cleared, when all log lines are selected.
Remove Ctrl+A/E hotkeys that move cursor to beginning/end respectively. Ctrl+A now selectes all text like for all other inputs. Home and End keys can still be used to go the beginning and end.
Remove Ctrl+U/K hotkeys that clear everything before/after the cursor respectively. Hold shift and use Home/End to select everything instead.
IME support
------------------------------
Render list of IME candidates in the client on Windows, so the candidate list can also be viewed in fullscreen mode. There is no API available to retrieve a candidate list on the other operating systems.
Improve composition rendering by underlining the composition text instead of putting it in square brackets.
Track active input globally to properly activate and deactivate IME through the SDL functions.
Closes #1030. Closes #1008.
Password rendering
------------------------------
Fix rendering of passwords containing unicode. Instead of rendering one star character for each UTF-8 `char`, render on star for every unicode codepoint.
Show the composition text also for passwords. Without seeing the composition text it's hard to type a password containing those characters. The candidate window exposes the composition anyway. If you don't want to expose your password this way, e.g. while streaming, you could:
1. Use a latin password and switch off the IME for the password input with the IME hotkey.
2. Blank your screen with an external program while you are streaming and entering passwords.
3. Use binds to authenticate in rcon or to set the server browser password.
Refactoring
------------------------------
Move all text input logic and general rendering to `CLineInput`.
A `CLineInput` is associated with a particular `char` buffer given as a pointer either in the constructor or with `SetBuffer`. The maximum byte size of the buffer must also be specified. The maximum length in unicode codepoints can also be specified separately (e.g. on upstream, name are limited by the number of unicode codepoints instead).
Add `CLineInputBuffered`, which is a `CLineInput` that own a `char` buffer of a fixed size, which is specified as a template argument. As `CLineInput` does not own a buffer anymore, this reduces duplicate code for line inputs that need their own buffer.
Add `CLineInputNumber` which has additional convenience functions to consider the text as an `int` or `float`, to reduce duplicate code in those cases. In the future we could also add an input filter function so that only numbers can be entered in the number input.
Add `CLineInput::SetClipboardLineCallback` to handle the case that multiple lines of text are pasted into a lineinput. This reduces duplicate code, as this behavior was previously implemented separately for chat and console. The behavior is also fixed to be consistent with the console on Windows, so the first line being pasted edits the current input text and then sends it instead of being sent on its own without the existing input text.
Add `CalcFontSizeAndBoundingBox` to UI to reduce duplicate code. Expose `CalcAlignedCursorPos` as static member function to reuse it for line input.
Dispatch input events to UI inputs through the event handler instead of storing them in a duplicate buffer.
Use `size_t` for line input cursor position, length etc. and for `str_utf8_stats`.
Add `IButtonColorFunction` to UI to describe a functions that defines colors for the Default, Active and Hovered states of UI elements. Add some default button color functions. Use button color function to reduce duplicate code in scrollbar rendering.
Use `vec2` instead of two `floats` to represent the mouse positions in the text renderer.
Remove `CaretPosition` again, as it does not calculate the correct Y position near line breaks due to the wrapping being different when not rendering the entire string. Instead, calculate the exact caret position when rending a text container and store the caret position in the text cursor for later use.
IME usage guide (Windows)
------------------------------
1. Install the respective language and the Microsoft-IME keyboard (e.g. for Chinese, Japanese or Korean).
2. Launch the game (or a text editor to first try out the IME). Note that Windows may track the input language separately for every application. You can change this in the Windows input settings so the input language is changed globally.
2. Switch the input language using the hotkey Windows+Space or another hotkey that you configured in the Windows input settings (Alt+Shift is the default, but you should consider disabling it, to avoid accidentally changing the input language while playing).
3. Switch from Latin/English input mode to the respective asian input mode.
- Chinese: Use Ctrl+Space to switch between English and Chinese input mode. You can change this hotkey in the IME's settings.
- Japanese: Use Ctrl+Space to switch between Alphanumeric and Hiragana/Katakana input mode. You can change this hotkey in the IME's settings.
- Korean: Use Right Alt to switch between English and Hangul input mode. You cannot change this hotkey as of yet.
- Note that the input mode is also tracked per application, but there is no setting to change this behavior as far as I know, so you'll need to switch for every application separately.
4. Start typing. The underlined text is the current composition text. While a composition is active, you can only edit the composition text. Confirm the composition with Space or by selecting a candidate from the candidate list with the arrow keys. Cancel the composition with Escape or by using Backspace to delete the composition text. Note that not all languages offer a candidate list.
SDL version-specific issues
------------------------------
- 2.26.5, 2.24.2, 2.0.22: IME candidates work. But there are minor bugs when moving the composition cursor.
- 2.0.18, 2.0.20: IME candidates work.
- 2.0.16 (our current version): IME candidates cannot be determined with Windows API. Windows tries to draw the composition window like before, so this does not work in fullscreen mode.
- 2.0.8 (upstream 0.7): IME candidates work. But this SDL version is too old for us.
2023-01-03 21:28:38 +00:00
pCursor - > m_CursorRenderedPosition = vec2 ( SelX , DrawY ) ;
2021-09-16 14:50:17 +00:00
}
}
2020-10-06 10:25:10 +00:00
pCursor - > m_MaxCharacterHeight = maximum ( pCursor - > m_MaxCharacterHeight , CharHeight + BearingY ) ;
2020-10-16 18:00:57 +00:00
if ( NextCharacter = = 0 & & ( RenderFlags & TEXT_RENDER_FLAG_NO_LAST_CHARACTER_ADVANCE ) ! = 0 & & Character ! = ' ' )
2019-01-06 05:42:57 +00:00
DrawX + = BearingX + CharKerning + CharWidth ;
else
2021-09-12 17:40:23 +00:00
DrawX + = Advance + CharKerning ;
2020-09-03 22:53:26 +00:00
pCursor - > m_GlyphCount + + ;
2021-09-12 17:40:23 +00:00
if ( SelectionStarted & & IsRendered )
{
2024-01-13 22:09:20 +00:00
if ( ! vSelectionQuads . empty ( ) & & SelectionQuadLine = = LineCount )
2023-12-27 17:13:10 +00:00
{
vSelectionQuads . back ( ) . m_Width + = SelWidth ;
}
else
{
const float SelectionHeight = pCursor - > m_AlignedFontSize + pCursor - > m_AlignedLineSpacing ;
const float SelectionY = DrawY + ( 1.0f - pCursor - > m_SelectionHeightFactor ) * SelectionHeight ;
const float ScaledSelectionHeight = pCursor - > m_SelectionHeightFactor * SelectionHeight ;
vSelectionQuads . emplace_back ( SelX , SelectionY , SelWidth , ScaledSelectionHeight ) ;
2024-01-13 22:09:20 +00:00
SelectionQuadLine = LineCount ;
2023-12-27 17:13:10 +00:00
}
2021-09-12 17:40:23 +00:00
}
LastSelX = SelX ;
LastSelWidth = SelWidth ;
LastCharX = CharX ;
LastCharWidth = CharWidth ;
2018-03-13 20:49:07 +00:00
}
2020-10-13 20:08:52 +00:00
2023-04-10 09:36:24 +00:00
pCursor - > m_LongestLineWidth = maximum ( pCursor - > m_LongestLineWidth , DrawX - pCursor - > m_StartX ) ;
2018-03-13 20:49:07 +00:00
}
if ( NewLine )
{
2023-09-06 16:04:14 +00:00
if ( ! StartNewLine ( ) )
break ;
2023-02-17 17:07:17 +00:00
GotNewLineLast = true ;
2018-03-13 20:49:07 +00:00
}
2020-10-28 14:56:02 +00:00
else
2023-02-17 17:07:17 +00:00
GotNewLineLast = false ;
2018-03-13 20:49:07 +00:00
}
2022-06-11 19:38:18 +00:00
if ( ! TextContainer . m_StringInfo . m_vCharacterQuads . empty ( ) & & IsRendered )
2018-03-13 20:49:07 +00:00
{
// setup the buffers
2020-08-29 10:10:38 +00:00
if ( Graphics ( ) - > IsTextBufferingEnabled ( ) )
2018-03-13 20:49:07 +00:00
{
2023-07-17 13:49:41 +00:00
const size_t DataSize = TextContainer . m_StringInfo . m_vCharacterQuads . size ( ) * sizeof ( STextCharQuad ) ;
2022-07-10 19:22:50 +00:00
void * pUploadData = TextContainer . m_StringInfo . m_vCharacterQuads . data ( ) ;
2018-03-13 20:49:07 +00:00
2020-10-22 20:21:19 +00:00
if ( TextContainer . m_StringInfo . m_QuadBufferObjectIndex ! = - 1 & & ( TextContainer . m_RenderFlags & TEXT_RENDER_FLAG_NO_AUTOMATIC_QUAD_UPLOAD ) = = 0 )
2018-03-13 20:49:07 +00:00
{
2022-03-20 17:03:25 +00:00
Graphics ( ) - > RecreateBufferObject ( TextContainer . m_StringInfo . m_QuadBufferObjectIndex , DataSize , pUploadData , TextContainer . m_SingleTimeUse ? IGraphics : : EBufferObjectCreateFlags : : BUFFER_OBJECT_CREATE_FLAGS_ONE_TIME_USE_BIT : 0 ) ;
2023-07-17 13:49:41 +00:00
Graphics ( ) - > IndicesNumRequiredNotify ( TextContainer . m_StringInfo . m_vCharacterQuads . size ( ) * 6 ) ;
2018-03-13 20:49:07 +00:00
}
}
}
2021-09-12 17:40:23 +00:00
if ( pCursor - > m_CalculateSelectionMode = = TEXT_CURSOR_SELECTION_MODE_CALCULATE )
{
pCursor - > m_SelectionStart = - 1 ;
pCursor - > m_SelectionEnd = - 1 ;
if ( SelectionStarted )
{
2023-07-17 13:49:41 +00:00
CheckSelectionEnd ( true , pCursor - > m_ReleaseMouse , SelectionEndChar , SelectionUsedRelease , std : : numeric_limits < float > : : max ( ) , 0 , DrawY + pCursor - > m_AlignedFontSize ) ;
CheckSelectionEnd ( true , pCursor - > m_PressMouse , SelectionStartChar , SelectionUsedPress , std : : numeric_limits < float > : : max ( ) , 0 , DrawY + pCursor - > m_AlignedFontSize ) ;
2021-09-12 17:40:23 +00:00
}
}
else if ( pCursor - > m_CalculateSelectionMode = = TEXT_CURSOR_SELECTION_MODE_SET )
{
2024-05-06 09:42:14 +00:00
if ( pCursor - > m_GlyphCount = = pCursor - > m_SelectionStart )
2021-09-16 14:50:17 +00:00
{
SelectionStarted = ! SelectionStarted ;
2023-04-13 21:59:06 +00:00
SelectionStartChar = pCursor - > m_GlyphCount ;
2021-09-16 14:50:17 +00:00
SelectionUsedPress = true ;
}
2024-05-06 09:42:14 +00:00
if ( pCursor - > m_GlyphCount = = pCursor - > m_SelectionEnd )
2021-09-12 17:40:23 +00:00
{
SelectionStarted = ! SelectionStarted ;
2023-04-13 21:59:06 +00:00
SelectionEndChar = pCursor - > m_GlyphCount ;
2021-09-12 17:40:23 +00:00
SelectionUsedRelease = true ;
}
}
2021-09-16 14:50:17 +00:00
if ( pCursor - > m_CursorMode ! = TEXT_CURSOR_CURSOR_MODE_NONE )
2021-09-12 17:40:23 +00:00
{
2023-07-17 13:49:41 +00:00
if ( pCursor - > m_CursorMode = = TEXT_CURSOR_CURSOR_MODE_CALCULATE & & pCursor - > m_CursorCharacter = = - 1 & & CheckOutsideChar ( true , pCursor - > m_ReleaseMouse , std : : numeric_limits < float > : : max ( ) , 0 , DrawY + pCursor - > m_AlignedFontSize ) )
2021-09-12 17:40:23 +00:00
{
2023-04-13 21:59:06 +00:00
pCursor - > m_CursorCharacter = pCursor - > m_GlyphCount ;
2021-09-16 14:50:17 +00:00
}
2021-09-12 17:40:23 +00:00
2024-05-06 09:42:14 +00:00
if ( pCursor - > m_GlyphCount = = pCursor - > m_CursorCharacter )
2021-09-16 14:50:17 +00:00
{
HasCursor = true ;
2023-07-17 13:49:41 +00:00
aCursorQuads [ 0 ] = IGraphics : : CQuadItem ( ( LastSelX + LastSelWidth ) - CursorOuterInnerDiff , DrawY , CursorOuterWidth , pCursor - > m_AlignedFontSize ) ;
aCursorQuads [ 1 ] = IGraphics : : CQuadItem ( ( LastSelX + LastSelWidth ) , DrawY + CursorOuterInnerDiff , CursorInnerWidth , pCursor - > m_AlignedFontSize - CursorOuterInnerDiff * 2 ) ;
Port line input and IME support from 0.7
Port the line input (UI edit boxes, chat, console) and Input Method Editor (IME) support from upstream. Closes #4397.
General
------------------------------
Fix issues with the text input. Closes #4346. Closes #4524.
Word skipping (when holding Ctrl) is overhauled to be consistent with the Windows / Firefox experience that I took as reference.
Improve usability by not blinking (i.e. always rendering) the caret shortly after is has been moved.
UI text input
------------------------------
Fix inconsistent mouse-based left and right scrolling (closes #4347).
Support smooth left and right scrolling.
Chat
------------------------------
Support keyboard-based text selection of the chat input.
Mouse-based selection could be support in the future when we decide to add something like an ingame UI cursor.
Support smooth up and down scrolling of the chat input, removing the old hack that offsets the input string to simulate scrolling.
Console
------------------------------
Also support mouse-based text selection of the command input.
Only text from either the command input or the console log can be selected at the same time. This ensures that Ctrl+C will always copy the text that is currently visually selected in the console.
Check for Ctrl+C input event in event handler instead of in render function, to hopefully fix the issue that copying does not work sometimes (closes #5974 until further notice).
When Ctrl+C is used to copy text from the console log, the selection is cleared. This should make it more clear when text was copied from the log.
Fix an issue that was preventing the console log selection from being cleared, when all log lines are selected.
Remove Ctrl+A/E hotkeys that move cursor to beginning/end respectively. Ctrl+A now selectes all text like for all other inputs. Home and End keys can still be used to go the beginning and end.
Remove Ctrl+U/K hotkeys that clear everything before/after the cursor respectively. Hold shift and use Home/End to select everything instead.
IME support
------------------------------
Render list of IME candidates in the client on Windows, so the candidate list can also be viewed in fullscreen mode. There is no API available to retrieve a candidate list on the other operating systems.
Improve composition rendering by underlining the composition text instead of putting it in square brackets.
Track active input globally to properly activate and deactivate IME through the SDL functions.
Closes #1030. Closes #1008.
Password rendering
------------------------------
Fix rendering of passwords containing unicode. Instead of rendering one star character for each UTF-8 `char`, render on star for every unicode codepoint.
Show the composition text also for passwords. Without seeing the composition text it's hard to type a password containing those characters. The candidate window exposes the composition anyway. If you don't want to expose your password this way, e.g. while streaming, you could:
1. Use a latin password and switch off the IME for the password input with the IME hotkey.
2. Blank your screen with an external program while you are streaming and entering passwords.
3. Use binds to authenticate in rcon or to set the server browser password.
Refactoring
------------------------------
Move all text input logic and general rendering to `CLineInput`.
A `CLineInput` is associated with a particular `char` buffer given as a pointer either in the constructor or with `SetBuffer`. The maximum byte size of the buffer must also be specified. The maximum length in unicode codepoints can also be specified separately (e.g. on upstream, name are limited by the number of unicode codepoints instead).
Add `CLineInputBuffered`, which is a `CLineInput` that own a `char` buffer of a fixed size, which is specified as a template argument. As `CLineInput` does not own a buffer anymore, this reduces duplicate code for line inputs that need their own buffer.
Add `CLineInputNumber` which has additional convenience functions to consider the text as an `int` or `float`, to reduce duplicate code in those cases. In the future we could also add an input filter function so that only numbers can be entered in the number input.
Add `CLineInput::SetClipboardLineCallback` to handle the case that multiple lines of text are pasted into a lineinput. This reduces duplicate code, as this behavior was previously implemented separately for chat and console. The behavior is also fixed to be consistent with the console on Windows, so the first line being pasted edits the current input text and then sends it instead of being sent on its own without the existing input text.
Add `CalcFontSizeAndBoundingBox` to UI to reduce duplicate code. Expose `CalcAlignedCursorPos` as static member function to reuse it for line input.
Dispatch input events to UI inputs through the event handler instead of storing them in a duplicate buffer.
Use `size_t` for line input cursor position, length etc. and for `str_utf8_stats`.
Add `IButtonColorFunction` to UI to describe a functions that defines colors for the Default, Active and Hovered states of UI elements. Add some default button color functions. Use button color function to reduce duplicate code in scrollbar rendering.
Use `vec2` instead of two `floats` to represent the mouse positions in the text renderer.
Remove `CaretPosition` again, as it does not calculate the correct Y position near line breaks due to the wrapping being different when not rendering the entire string. Instead, calculate the exact caret position when rending a text container and store the caret position in the text cursor for later use.
IME usage guide (Windows)
------------------------------
1. Install the respective language and the Microsoft-IME keyboard (e.g. for Chinese, Japanese or Korean).
2. Launch the game (or a text editor to first try out the IME). Note that Windows may track the input language separately for every application. You can change this in the Windows input settings so the input language is changed globally.
2. Switch the input language using the hotkey Windows+Space or another hotkey that you configured in the Windows input settings (Alt+Shift is the default, but you should consider disabling it, to avoid accidentally changing the input language while playing).
3. Switch from Latin/English input mode to the respective asian input mode.
- Chinese: Use Ctrl+Space to switch between English and Chinese input mode. You can change this hotkey in the IME's settings.
- Japanese: Use Ctrl+Space to switch between Alphanumeric and Hiragana/Katakana input mode. You can change this hotkey in the IME's settings.
- Korean: Use Right Alt to switch between English and Hangul input mode. You cannot change this hotkey as of yet.
- Note that the input mode is also tracked per application, but there is no setting to change this behavior as far as I know, so you'll need to switch for every application separately.
4. Start typing. The underlined text is the current composition text. While a composition is active, you can only edit the composition text. Confirm the composition with Space or by selecting a candidate from the candidate list with the arrow keys. Cancel the composition with Escape or by using Backspace to delete the composition text. Note that not all languages offer a candidate list.
SDL version-specific issues
------------------------------
- 2.26.5, 2.24.2, 2.0.22: IME candidates work. But there are minor bugs when moving the composition cursor.
- 2.0.18, 2.0.20: IME candidates work.
- 2.0.16 (our current version): IME candidates cannot be determined with Windows API. Windows tries to draw the composition window like before, so this does not work in fullscreen mode.
- 2.0.8 (upstream 0.7): IME candidates work. But this SDL version is too old for us.
2023-01-03 21:28:38 +00:00
pCursor - > m_CursorRenderedPosition = vec2 ( LastSelX + LastSelWidth , DrawY ) ;
2021-09-12 17:40:23 +00:00
}
}
2023-02-17 17:07:17 +00:00
const bool HasSelection = ! vSelectionQuads . empty ( ) & & SelectionUsedPress & & SelectionUsedRelease ;
2021-09-16 14:50:17 +00:00
if ( ( HasSelection | | HasCursor ) & & IsRendered )
{
Graphics ( ) - > SetColor ( 1.f , 1.f , 1.f , 1.f ) ;
if ( TextContainer . m_StringInfo . m_SelectionQuadContainerIndex = = - 1 )
TextContainer . m_StringInfo . m_SelectionQuadContainerIndex = Graphics ( ) - > CreateQuadContainer ( false ) ;
if ( HasCursor )
2023-12-11 21:14:45 +00:00
Graphics ( ) - > QuadContainerAddQuads ( TextContainer . m_StringInfo . m_SelectionQuadContainerIndex , aCursorQuads , std : : size ( aCursorQuads ) ) ;
2021-09-16 14:50:17 +00:00
if ( HasSelection )
2023-12-11 21:14:45 +00:00
Graphics ( ) - > QuadContainerAddQuads ( TextContainer . m_StringInfo . m_SelectionQuadContainerIndex , vSelectionQuads . data ( ) , vSelectionQuads . size ( ) ) ;
2021-09-16 14:50:17 +00:00
Graphics ( ) - > QuadContainerUpload ( TextContainer . m_StringInfo . m_SelectionQuadContainerIndex ) ;
TextContainer . m_HasCursor = HasCursor ;
TextContainer . m_HasSelection = HasSelection ;
2023-04-14 13:05:34 +00:00
TextContainer . m_ForceCursorRendering = pCursor - > m_ForceCursorRendering ;
2021-09-16 14:50:17 +00:00
2021-11-02 23:08:00 +00:00
if ( HasSelection )
{
pCursor - > m_SelectionStart = SelectionStartChar ;
pCursor - > m_SelectionEnd = SelectionEndChar ;
}
else
{
pCursor - > m_SelectionStart = - 1 ;
pCursor - > m_SelectionEnd = - 1 ;
}
2021-09-16 14:50:17 +00:00
}
2018-03-13 20:49:07 +00:00
// even if no text is drawn the cursor position will be adjusted
pCursor - > m_X = DrawX ;
2024-04-29 06:04:24 +00:00
pCursor - > m_Y = DrawY ;
2010-10-11 00:29:30 +00:00
pCursor - > m_LineCount = LineCount ;
2011-04-13 18:37:12 +00:00
2023-04-10 09:04:28 +00:00
TextContainer . m_BoundingBox = pCursor - > BoundingBox ( ) ;
2010-05-29 07:25:38 +00:00
}
2011-04-13 18:37:12 +00:00
2023-05-05 13:58:17 +00:00
bool CreateOrAppendTextContainer ( STextContainerIndex & TextContainerIndex , CTextCursor * pCursor , const char * pText , int Length = - 1 ) override
2022-07-08 16:51:19 +00:00
{
2023-05-05 13:58:17 +00:00
if ( TextContainerIndex . Valid ( ) )
2022-07-08 16:51:19 +00:00
{
2023-05-05 13:58:17 +00:00
AppendTextContainer ( TextContainerIndex , pCursor , pText , Length ) ;
return true ;
2022-07-08 16:51:19 +00:00
}
else
{
2023-05-05 13:58:17 +00:00
return CreateTextContainer ( TextContainerIndex , pCursor , pText , Length ) ;
2022-07-08 16:51:19 +00:00
}
}
2018-03-13 20:49:07 +00:00
// just deletes and creates text container
2023-05-05 13:58:17 +00:00
void RecreateTextContainer ( STextContainerIndex & TextContainerIndex , CTextCursor * pCursor , const char * pText , int Length = - 1 ) override
2018-03-13 20:49:07 +00:00
{
DeleteTextContainer ( TextContainerIndex ) ;
2022-07-08 16:51:19 +00:00
CreateTextContainer ( TextContainerIndex , pCursor , pText , Length ) ;
2018-03-13 20:49:07 +00:00
}
2023-05-05 13:58:17 +00:00
void RecreateTextContainerSoft ( STextContainerIndex & TextContainerIndex , CTextCursor * pCursor , const char * pText , int Length = - 1 ) override
2018-03-13 20:49:07 +00:00
{
2020-09-26 19:41:58 +00:00
STextContainer & TextContainer = GetTextContainer ( TextContainerIndex ) ;
2022-06-11 19:38:18 +00:00
TextContainer . m_StringInfo . m_vCharacterQuads . clear ( ) ;
2018-03-13 20:49:07 +00:00
// the text buffer gets then recreated by the appended quads
2022-07-08 16:51:19 +00:00
AppendTextContainer ( TextContainerIndex , pCursor , pText , Length ) ;
2018-03-13 20:49:07 +00:00
}
2023-05-05 13:58:17 +00:00
void DeleteTextContainer ( STextContainerIndex & TextContainerIndex ) override
2018-03-13 20:49:07 +00:00
{
2023-05-05 13:58:17 +00:00
if ( ! TextContainerIndex . Valid ( ) )
2023-02-14 23:39:47 +00:00
return ;
STextContainer & TextContainer = GetTextContainer ( TextContainerIndex ) ;
if ( Graphics ( ) - > IsTextBufferingEnabled ( ) )
Graphics ( ) - > DeleteBufferContainer ( TextContainer . m_StringInfo . m_QuadBufferContainerIndex , true ) ;
Graphics ( ) - > DeleteQuadContainer ( TextContainer . m_StringInfo . m_SelectionQuadContainerIndex ) ;
FreeTextContainer ( TextContainerIndex ) ;
2018-03-13 20:49:07 +00:00
}
2023-05-05 13:58:17 +00:00
void UploadTextContainer ( STextContainerIndex TextContainerIndex ) override
2020-10-22 20:21:19 +00:00
{
2021-09-24 23:57:54 +00:00
if ( Graphics ( ) - > IsTextBufferingEnabled ( ) )
{
STextContainer & TextContainer = GetTextContainer ( TextContainerIndex ) ;
2022-06-11 19:38:18 +00:00
size_t DataSize = TextContainer . m_StringInfo . m_vCharacterQuads . size ( ) * sizeof ( STextCharQuad ) ;
void * pUploadData = TextContainer . m_StringInfo . m_vCharacterQuads . data ( ) ;
2022-03-20 17:03:25 +00:00
TextContainer . m_StringInfo . m_QuadBufferObjectIndex = Graphics ( ) - > CreateBufferObject ( DataSize , pUploadData , TextContainer . m_SingleTimeUse ? IGraphics : : EBufferObjectCreateFlags : : BUFFER_OBJECT_CREATE_FLAGS_ONE_TIME_USE_BIT : 0 ) ;
2020-10-22 20:21:19 +00:00
2022-03-20 17:03:25 +00:00
m_DefaultTextContainerInfo . m_VertBufferBindingIndex = TextContainer . m_StringInfo . m_QuadBufferObjectIndex ;
2020-10-22 20:21:19 +00:00
2021-09-24 23:57:54 +00:00
TextContainer . m_StringInfo . m_QuadBufferContainerIndex = Graphics ( ) - > CreateBufferContainer ( & m_DefaultTextContainerInfo ) ;
2023-07-17 13:49:41 +00:00
Graphics ( ) - > IndicesNumRequiredNotify ( TextContainer . m_StringInfo . m_vCharacterQuads . size ( ) * 6 ) ;
2021-09-24 23:57:54 +00:00
}
2020-10-22 20:21:19 +00:00
}
2023-05-05 13:58:17 +00:00
void RenderTextContainer ( STextContainerIndex TextContainerIndex , const ColorRGBA & TextColor , const ColorRGBA & TextOutlineColor ) override
2018-03-13 20:49:07 +00:00
{
2023-02-17 17:07:17 +00:00
const STextContainer & TextContainer = GetTextContainer ( TextContainerIndex ) ;
2018-03-13 20:49:07 +00:00
2023-07-17 13:49:41 +00:00
if ( ! TextContainer . m_StringInfo . m_vCharacterQuads . empty ( ) )
2018-03-13 20:49:07 +00:00
{
2021-09-16 14:50:17 +00:00
if ( Graphics ( ) - > IsTextBufferingEnabled ( ) )
2018-03-13 20:49:07 +00:00
{
2021-09-16 14:50:17 +00:00
Graphics ( ) - > TextureClear ( ) ;
// render buffered text
2023-07-17 13:49:41 +00:00
Graphics ( ) - > RenderText ( TextContainer . m_StringInfo . m_QuadBufferContainerIndex , TextContainer . m_StringInfo . m_vCharacterQuads . size ( ) , m_pGlyphMap - > TextureDimension ( ) , m_pGlyphMap - > Texture ( CGlyphMap : : FONT_TEXTURE_FILL ) . Id ( ) , m_pGlyphMap - > Texture ( CGlyphMap : : FONT_TEXTURE_OUTLINE ) . Id ( ) , TextColor , TextOutlineColor ) ;
2018-03-13 20:49:07 +00:00
}
2021-09-16 14:50:17 +00:00
else
2018-03-13 20:49:07 +00:00
{
2021-09-16 14:50:17 +00:00
// render tiles
2023-07-17 13:49:41 +00:00
const float UVScale = 1.0f / m_pGlyphMap - > TextureDimension ( ) ;
2018-05-07 03:52:02 +00:00
2021-09-16 14:50:17 +00:00
Graphics ( ) - > FlushVertices ( ) ;
2023-07-17 13:49:41 +00:00
Graphics ( ) - > TextureSet ( m_pGlyphMap - > Texture ( CGlyphMap : : FONT_TEXTURE_OUTLINE ) ) ;
2021-09-16 14:50:17 +00:00
Graphics ( ) - > QuadsBegin ( ) ;
2019-04-26 19:36:49 +00:00
2023-07-17 13:49:41 +00:00
for ( const STextCharQuad & TextCharQuad : TextContainer . m_StringInfo . m_vCharacterQuads )
2018-04-03 15:40:21 +00:00
{
2022-06-30 22:36:32 +00:00
Graphics ( ) - > SetColor ( TextCharQuad . m_aVertices [ 0 ] . m_Color . r / 255.f * TextOutlineColor . r , TextCharQuad . m_aVertices [ 0 ] . m_Color . g / 255.f * TextOutlineColor . g , TextCharQuad . m_aVertices [ 0 ] . m_Color . b / 255.f * TextOutlineColor . b , TextCharQuad . m_aVertices [ 0 ] . m_Color . a / 255.f * TextOutlineColor . a ) ;
Graphics ( ) - > QuadsSetSubset ( TextCharQuad . m_aVertices [ 0 ] . m_U * UVScale , TextCharQuad . m_aVertices [ 0 ] . m_V * UVScale , TextCharQuad . m_aVertices [ 2 ] . m_U * UVScale , TextCharQuad . m_aVertices [ 2 ] . m_V * UVScale ) ;
IGraphics : : CQuadItem QuadItem ( TextCharQuad . m_aVertices [ 0 ] . m_X , TextCharQuad . m_aVertices [ 0 ] . m_Y , TextCharQuad . m_aVertices [ 1 ] . m_X - TextCharQuad . m_aVertices [ 0 ] . m_X , TextCharQuad . m_aVertices [ 2 ] . m_Y - TextCharQuad . m_aVertices [ 0 ] . m_Y ) ;
2021-09-16 14:50:17 +00:00
Graphics ( ) - > QuadsDrawTL ( & QuadItem , 1 ) ;
}
2022-07-01 04:42:36 +00:00
if ( TextColor . a ! = 0 )
2021-09-16 14:50:17 +00:00
{
Graphics ( ) - > QuadsEndKeepVertices ( ) ;
2023-07-17 13:49:41 +00:00
Graphics ( ) - > TextureSet ( m_pGlyphMap - > Texture ( CGlyphMap : : FONT_TEXTURE_FILL ) ) ;
2021-09-16 14:50:17 +00:00
2023-07-17 13:49:41 +00:00
int TextCharQuadIndex = 0 ;
for ( const STextCharQuad & TextCharQuad : TextContainer . m_StringInfo . m_vCharacterQuads )
2021-09-16 14:50:17 +00:00
{
2022-06-30 22:36:32 +00:00
unsigned char CR = ( unsigned char ) ( ( float ) ( TextCharQuad . m_aVertices [ 0 ] . m_Color . r ) * TextColor . r ) ;
unsigned char CG = ( unsigned char ) ( ( float ) ( TextCharQuad . m_aVertices [ 0 ] . m_Color . g ) * TextColor . g ) ;
unsigned char CB = ( unsigned char ) ( ( float ) ( TextCharQuad . m_aVertices [ 0 ] . m_Color . b ) * TextColor . b ) ;
unsigned char CA = ( unsigned char ) ( ( float ) ( TextCharQuad . m_aVertices [ 0 ] . m_Color . a ) * TextColor . a ) ;
2023-07-17 13:49:41 +00:00
Graphics ( ) - > ChangeColorOfQuadVertices ( TextCharQuadIndex , CR , CG , CB , CA ) ;
+ + TextCharQuadIndex ;
2021-09-16 14:50:17 +00:00
}
// render non outlined
Graphics ( ) - > QuadsDrawCurrentVertices ( false ) ;
2018-04-03 15:40:21 +00:00
}
2021-09-16 14:50:17 +00:00
else
Graphics ( ) - > QuadsEnd ( ) ;
2019-04-26 19:36:49 +00:00
2021-09-16 14:50:17 +00:00
// reset
Graphics ( ) - > SetColor ( 1.f , 1.f , 1.f , 1.f ) ;
2018-03-13 20:49:07 +00:00
}
2021-09-16 14:50:17 +00:00
}
2018-03-13 20:49:07 +00:00
2023-04-14 13:04:51 +00:00
if ( TextContainer . m_StringInfo . m_SelectionQuadContainerIndex ! = - 1 )
2021-09-16 14:50:17 +00:00
{
2023-04-14 13:04:51 +00:00
if ( TextContainer . m_HasSelection )
{
Graphics ( ) - > TextureClear ( ) ;
Graphics ( ) - > SetColor ( m_SelectionColor ) ;
Graphics ( ) - > RenderQuadContainerEx ( TextContainer . m_StringInfo . m_SelectionQuadContainerIndex , TextContainer . m_HasCursor ? 2 : 0 , - 1 , 0 , 0 ) ;
Graphics ( ) - > SetColor ( 1.0f , 1.0f , 1.0f , 1.0f ) ;
}
2021-09-16 14:50:17 +00:00
2023-04-14 13:04:51 +00:00
if ( TextContainer . m_HasCursor )
2023-02-17 17:07:17 +00:00
{
2023-04-14 13:04:51 +00:00
const auto CurTime = time_get_nanoseconds ( ) ;
Graphics ( ) - > TextureClear ( ) ;
2023-04-14 13:05:34 +00:00
if ( TextContainer . m_ForceCursorRendering | | ( CurTime - m_CursorRenderTime ) > 500 ms )
2023-04-14 13:04:51 +00:00
{
Graphics ( ) - > SetColor ( TextOutlineColor ) ;
Graphics ( ) - > RenderQuadContainerEx ( TextContainer . m_StringInfo . m_SelectionQuadContainerIndex , 0 , 1 , 0 , 0 ) ;
Graphics ( ) - > SetColor ( TextColor ) ;
Graphics ( ) - > RenderQuadContainerEx ( TextContainer . m_StringInfo . m_SelectionQuadContainerIndex , 1 , 1 , 0 , 0 ) ;
}
2023-04-14 13:05:34 +00:00
if ( TextContainer . m_ForceCursorRendering )
m_CursorRenderTime = CurTime - 501 ms ;
else if ( ( CurTime - m_CursorRenderTime ) > 1 s )
2023-04-14 13:04:51 +00:00
m_CursorRenderTime = time_get_nanoseconds ( ) ;
Graphics ( ) - > SetColor ( 1.0f , 1.0f , 1.0f , 1.0f ) ;
2021-09-16 14:50:17 +00:00
}
2018-03-13 20:49:07 +00:00
}
}
2023-05-05 13:58:17 +00:00
void RenderTextContainer ( STextContainerIndex TextContainerIndex , const ColorRGBA & TextColor , const ColorRGBA & TextOutlineColor , float X , float Y ) override
2018-03-13 20:49:07 +00:00
{
2023-04-10 09:04:28 +00:00
STextContainer & TextContainer = GetTextContainer ( TextContainerIndex ) ;
2018-03-21 14:43:56 +00:00
2018-03-13 20:49:07 +00:00
// remap the current screen, after render revert the change again
float ScreenX0 , ScreenY0 , ScreenX1 , ScreenY1 ;
2019-04-26 19:36:49 +00:00
Graphics ( ) - > GetScreen ( & ScreenX0 , & ScreenY0 , & ScreenX1 , & ScreenY1 ) ;
2020-09-26 19:41:58 +00:00
if ( ( TextContainer . m_RenderFlags & TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT ) = = 0 )
2018-03-21 14:43:56 +00:00
{
2023-07-17 13:49:41 +00:00
const vec2 FakeToScreen = vec2 ( Graphics ( ) - > ScreenWidth ( ) / ( ScreenX1 - ScreenX0 ) , Graphics ( ) - > ScreenHeight ( ) / ( ScreenY1 - ScreenY0 ) ) ;
const float AlignedX = round_to_int ( ( TextContainer . m_X + X ) * FakeToScreen . x ) / FakeToScreen . x ;
const float AlignedY = round_to_int ( ( TextContainer . m_Y + Y ) * FakeToScreen . y ) / FakeToScreen . y ;
2018-03-21 14:43:56 +00:00
X = AlignedX - TextContainer . m_AlignedStartX ;
Y = AlignedY - TextContainer . m_AlignedStartY ;
}
2019-04-26 19:36:49 +00:00
2023-04-10 09:04:28 +00:00
TextContainer . m_BoundingBox . m_X = X ;
TextContainer . m_BoundingBox . m_Y = Y ;
2018-03-13 20:49:07 +00:00
Graphics ( ) - > MapScreen ( ScreenX0 - X , ScreenY0 - Y , ScreenX1 - X , ScreenY1 - Y ) ;
2022-07-01 04:42:36 +00:00
RenderTextContainer ( TextContainerIndex , TextColor , TextOutlineColor ) ;
2018-03-13 20:49:07 +00:00
Graphics ( ) - > MapScreen ( ScreenX0 , ScreenY0 , ScreenX1 , ScreenY1 ) ;
}
2023-05-05 13:58:17 +00:00
STextBoundingBox GetBoundingBoxTextContainer ( STextContainerIndex TextContainerIndex ) override
2023-04-10 09:04:28 +00:00
{
const STextContainer & TextContainer = GetTextContainer ( TextContainerIndex ) ;
return TextContainer . m_BoundingBox ;
}
2024-03-24 12:27:31 +00:00
void UploadEntityLayerText ( const CImageInfo & TextImage , int TexSubWidth , int TexSubHeight , const char * pText , int Length , float x , float y , int FontSize ) override
2017-09-12 18:10:27 +00:00
{
2024-03-24 12:27:31 +00:00
m_pGlyphMap - > UploadEntityLayerText ( TextImage , TexSubWidth , TexSubHeight , pText , Length , x , y , FontSize ) ;
2019-05-06 12:19:10 +00:00
}
2020-03-20 12:48:45 +00:00
2023-02-17 17:07:17 +00:00
int AdjustFontSize ( const char * pText , int TextLength , int MaxSize , int MaxWidth ) const override
2019-05-06 12:19:10 +00:00
{
2023-02-17 17:07:17 +00:00
const int WidthOfText = CalculateTextWidth ( pText , TextLength , 0 , 100 ) ;
2019-05-06 12:19:10 +00:00
2023-07-17 13:49:41 +00:00
int FontSize = 100.0f / ( ( float ) WidthOfText / ( float ) MaxWidth ) ;
2020-09-26 19:41:58 +00:00
if ( MaxSize > 0 & & FontSize > MaxSize )
2019-05-06 12:19:10 +00:00
FontSize = MaxSize ;
return FontSize ;
}
2023-02-17 17:07:17 +00:00
float GetGlyphOffsetX ( int FontSize , char TextCharacter ) const override
2020-10-16 18:00:57 +00:00
{
2023-07-17 13:49:41 +00:00
if ( m_pGlyphMap - > DefaultFace ( ) = = nullptr )
return - 1.0f ;
2023-04-05 19:47:49 +00:00
2023-07-17 13:49:41 +00:00
FT_Set_Pixel_Sizes ( m_pGlyphMap - > DefaultFace ( ) , 0 , FontSize ) ;
2020-10-16 18:00:57 +00:00
const char * pTmp = & TextCharacter ;
2023-02-17 17:07:17 +00:00
const int NextCharacter = str_utf8_decode ( & pTmp ) ;
2020-10-16 18:00:57 +00:00
if ( NextCharacter )
{
# if FREETYPE_MAJOR >= 2 && FREETYPE_MINOR >= 7 && (FREETYPE_MINOR > 7 || FREETYPE_PATCH >= 1)
2023-02-17 17:07:17 +00:00
const FT_Int32 FTFlags = FT_LOAD_BITMAP_METRICS_ONLY | FT_LOAD_NO_BITMAP ;
2020-10-16 18:00:57 +00:00
# else
2023-02-17 17:07:17 +00:00
const FT_Int32 FTFlags = FT_LOAD_RENDER | FT_LOAD_NO_BITMAP ;
2020-10-16 18:00:57 +00:00
# endif
2023-07-17 13:49:41 +00:00
if ( FT_Load_Char ( m_pGlyphMap - > DefaultFace ( ) , NextCharacter , FTFlags ) )
2020-10-16 18:00:57 +00:00
{
2023-07-17 13:49:41 +00:00
log_debug ( " textrender " , " Error loading glyph. Chr=%d " , NextCharacter ) ;
return - 1.0f ;
2020-10-16 18:00:57 +00:00
}
2023-07-17 13:49:41 +00:00
return ( float ) ( m_pGlyphMap - > DefaultFace ( ) - > glyph - > metrics . horiBearingX > > 6 ) ;
2020-10-16 18:00:57 +00:00
}
2023-07-17 13:49:41 +00:00
return 0.0f ;
2020-10-16 18:00:57 +00:00
}
2023-02-17 17:07:17 +00:00
int CalculateTextWidth ( const char * pText , int TextLength , int FontWidth , int FontHeight ) const override
2019-05-06 12:19:10 +00:00
{
2023-07-17 13:49:41 +00:00
if ( m_pGlyphMap - > DefaultFace ( ) = = nullptr )
2023-04-05 19:47:49 +00:00
return 0 ;
2023-02-17 17:07:17 +00:00
const char * pCurrent = pText ;
2019-05-06 12:19:10 +00:00
const char * pEnd = pCurrent + TextLength ;
2019-04-26 19:36:49 +00:00
2019-05-06 12:19:10 +00:00
int WidthOfText = 0 ;
2023-07-17 13:49:41 +00:00
FT_Set_Pixel_Sizes ( m_pGlyphMap - > DefaultFace ( ) , FontWidth , FontHeight ) ;
2020-09-26 19:41:58 +00:00
while ( pCurrent < pEnd )
2019-05-06 12:19:10 +00:00
{
const char * pTmp = pCurrent ;
2023-02-17 17:07:17 +00:00
const int NextCharacter = str_utf8_decode ( & pTmp ) ;
2019-05-06 12:19:10 +00:00
if ( NextCharacter )
{
# if FREETYPE_MAJOR >= 2 && FREETYPE_MINOR >= 7 && (FREETYPE_MINOR > 7 || FREETYPE_PATCH >= 1)
2023-02-17 17:07:17 +00:00
const FT_Int32 FTFlags = FT_LOAD_BITMAP_METRICS_ONLY | FT_LOAD_NO_BITMAP ;
2019-05-06 12:19:10 +00:00
# else
2023-02-17 17:07:17 +00:00
const FT_Int32 FTFlags = FT_LOAD_RENDER | FT_LOAD_NO_BITMAP ;
2019-05-06 12:19:10 +00:00
# endif
2023-07-17 13:49:41 +00:00
if ( FT_Load_Char ( m_pGlyphMap - > DefaultFace ( ) , NextCharacter , FTFlags ) )
2019-05-06 12:19:10 +00:00
{
2023-07-17 13:49:41 +00:00
log_debug ( " textrender " , " Error loading glyph. Chr=%d " , NextCharacter ) ;
2019-05-06 12:19:10 +00:00
pCurrent = pTmp ;
continue ;
2017-09-12 18:10:27 +00:00
}
2019-05-06 12:19:10 +00:00
2023-07-17 13:49:41 +00:00
WidthOfText + = ( m_pGlyphMap - > DefaultFace ( ) - > glyph - > metrics . width > > 6 ) + 1 ;
2017-09-12 18:10:27 +00:00
}
pCurrent = pTmp ;
}
2018-03-21 14:43:56 +00:00
2019-05-06 12:19:10 +00:00
return WidthOfText ;
}
2020-03-20 12:48:45 +00:00
2023-05-14 17:49:24 +00:00
void OnPreWindowResize ( ) override
{
for ( auto * pTextContainer : m_vpTextContainers )
{
if ( pTextContainer - > m_ContainerIndex . Valid ( ) & & pTextContainer - > m_ContainerIndex . m_UseCount . use_count ( ) < = 1 )
{
2023-07-17 13:49:41 +00:00
log_error ( " textrender " , " Found non empty text container with index %d with % " PRIzu " quads '%s' " , pTextContainer - > m_StringInfo . m_QuadBufferContainerIndex , pTextContainer - > m_StringInfo . m_vCharacterQuads . size ( ) , pTextContainer - > m_aDebugText ) ;
2023-05-14 17:49:24 +00:00
dbg_assert ( false , " Text container was forgotten by the implementation (the index was overwritten). " ) ;
}
}
}
2022-05-17 18:33:27 +00:00
void OnWindowResize ( ) override
2018-03-21 14:43:56 +00:00
{
2022-05-18 16:23:02 +00:00
bool HasNonEmptyTextContainer = false ;
2022-06-11 19:38:18 +00:00
for ( auto * pTextContainer : m_vpTextContainers )
2018-03-21 15:07:03 +00:00
{
2022-02-09 12:25:00 +00:00
if ( pTextContainer - > m_StringInfo . m_QuadBufferContainerIndex ! = - 1 )
{
2023-07-17 13:49:41 +00:00
log_error ( " textrender " , " Found non empty text container with index %d with % " PRIzu " quads '%s' " , pTextContainer - > m_StringInfo . m_QuadBufferContainerIndex , pTextContainer - > m_StringInfo . m_vCharacterQuads . size ( ) , pTextContainer - > m_aDebugText ) ;
log_error ( " textrender " , " The text container index was in use by %d " , ( int ) pTextContainer - > m_ContainerIndex . m_UseCount . use_count ( ) ) ;
2023-10-08 12:43:37 +00:00
HasNonEmptyTextContainer = true ;
2022-02-09 12:25:00 +00:00
}
2018-03-21 15:07:03 +00:00
}
2018-03-21 14:43:56 +00:00
2022-05-18 16:23:02 +00:00
dbg_assert ( ! HasNonEmptyTextContainer , " text container was not empty " ) ;
2018-03-21 14:43:56 +00:00
}
2010-05-29 07:25:38 +00:00
} ;
IEngineTextRender * CreateEngineTextRender ( ) { return new CTextRender ; }