Files
m2dev-client-src/src/EterLib/GrpTextInstance.cpp
2026-02-18 15:51:17 +00:00

1414 lines
37 KiB
C++

#include "StdAfx.h"
#include "GrpTextInstance.h"
#include "StateManager.h"
#include "IME.h"
#include "TextTag.h"
#include "EterBase/Utils.h"
#include "EterLocale/Arabic.h"
#include <unordered_map>
#include <utf8.h>
// Forward declaration to avoid header conflicts
extern bool IsRTL();
const float c_fFontFeather = 0.5f;
CDynamicPool<CGraphicTextInstance> CGraphicTextInstance::ms_kPool;
static int gs_mx = 0;
static int gs_my = 0;
static std::wstring gs_hyperlinkText;
void CGraphicTextInstance::Hyperlink_UpdateMousePos(int x, int y)
{
gs_mx = x;
gs_my = y;
gs_hyperlinkText = L"";
}
int CGraphicTextInstance::Hyperlink_GetText(char* buf, int len)
{
if (gs_hyperlinkText.empty())
return 0;
int written = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, gs_hyperlinkText.c_str(), (int)gs_hyperlinkText.length(), buf, len, nullptr, nullptr);
return (written > 0) ? written : 0;
}
int CGraphicTextInstance::__DrawCharacter(CGraphicFontTexture * pFontTexture, wchar_t text, DWORD dwColor, wchar_t prevChar)
{
CGraphicFontTexture::TCharacterInfomation* pInsCharInfo = pFontTexture->GetCharacterInfomation(text);
if (pInsCharInfo)
{
// Round kerning to nearest pixel to keep glyphs on the pixel grid.
// Fractional offsets cause bilinear interpolation blur in D3D9.
float kern = floorf(pFontTexture->GetKerning(prevChar, text) + 0.5f);
m_dwColorInfoVector.push_back(dwColor);
m_pCharInfoVector.push_back(pInsCharInfo);
m_kernVector.push_back(kern);
m_textWidth += (int)(pInsCharInfo->advance + kern);
m_textHeight = std::max((WORD)pInsCharInfo->height, m_textHeight);
return (int)(pInsCharInfo->advance + kern);
}
return 0;
}
void CGraphicTextInstance::__GetTextPos(DWORD index, float* x, float* y)
{
index = std::min((size_t)index, m_pCharInfoVector.size());
float sx = 0;
float sy = 0;
float fFontMaxHeight = 0;
for(DWORD i=0; i<index; ++i)
{
if (sx+float(m_pCharInfoVector[i]->width) > m_fLimitWidth)
{
sx = 0;
sy += fFontMaxHeight;
}
sx += float(m_pCharInfoVector[i]->advance);
fFontMaxHeight = std::max(float(m_pCharInfoVector[i]->height), fFontMaxHeight);
}
*x = sx;
*y = sy;
}
void CGraphicTextInstance::Update()
{
if (m_isUpdate)
return;
// Get space height first for empty text cursor rendering
int spaceHeight = 12; // default fallback
if (!m_roText.IsNull() && !m_roText->IsEmpty())
{
CGraphicFontTexture* pFontTexture = m_roText->GetFontTexturePointer();
if (pFontTexture)
{
CGraphicFontTexture::TCharacterInfomation* pSpaceInfo = pFontTexture->GetCharacterInfomation(L' ');
spaceHeight = pSpaceInfo ? pSpaceInfo->height : 12;
}
}
auto ResetState = [&, spaceHeight]()
{
m_pCharInfoVector.clear();
m_kernVector.clear();
m_dwColorInfoVector.clear();
m_hyperlinkVector.clear();
m_textWidth = 0;
m_textHeight = spaceHeight; // Use space height instead of 0 for cursor rendering
m_computedRTL = IsRTL(); // Use global RTL setting
m_isUpdate = true;
};
if (m_roText.IsNull() || m_roText->IsEmpty())
{
ResetState();
return;
}
CGraphicFontTexture* pFontTexture = m_roText->GetFontTexturePointer();
if (!pFontTexture)
{
ResetState();
return;
}
m_pCharInfoVector.clear();
m_kernVector.clear();
m_dwColorInfoVector.clear();
m_hyperlinkVector.clear();
m_textWidth = 0;
m_textHeight = spaceHeight;
const char* utf8 = m_stText.c_str();
const int utf8Len = (int)m_stText.size();
DWORD dwColor = m_dwTextColor;
// UTF-8 -> UTF-16 conversion
std::vector<wchar_t> wTextBuf((size_t)utf8Len + 1u, 0);
int wTextLen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8, utf8Len, wTextBuf.data(), (int)wTextBuf.size());
// If strict UTF-8 conversion fails, try lenient mode (replaces invalid sequences)
if (wTextLen <= 0)
{
// Try lenient conversion (no MB_ERR_INVALID_CHARS flag)
wTextLen = MultiByteToWideChar(CP_UTF8, 0, utf8, utf8Len, wTextBuf.data(), (int)wTextBuf.size());
if (wTextLen <= 0)
{
ResetState();
return;
}
}
// Set computed RTL based on global setting
m_computedRTL = IsRTL();
// Secret mode: draw '*' instead of actual characters
if (m_isSecret)
{
wchar_t prevCh = 0;
for (int i = 0; i < wTextLen; ++i)
{
__DrawCharacter(pFontTexture, L'*', dwColor, prevCh);
prevCh = L'*';
}
pFontTexture->UpdateTexture();
m_isUpdate = true;
return;
}
// === RENDERING APPROACH ===
// Use BuildVisualBidiText_Tagless() and BuildVisualChatMessage() from utf8.h
// These functions handle Arabic shaping, BiDi reordering, and chat formatting properly
// Special handling for chat messages
if (m_isChatMessage && !m_chatName.empty() && !m_chatMessage.empty())
{
std::wstring wName = Utf8ToWide(m_chatName);
std::wstring wMsg = Utf8ToWide(m_chatMessage);
// Check if message has tags (hyperlinks)
bool msgHasTags = (std::find(wMsg.begin(), wMsg.end(), L'|') != wMsg.end());
if (!msgHasTags)
{
// No tags: Use BuildVisualChatMessage() for simple BiDi
std::vector<wchar_t> visual = BuildVisualChatMessage(
wName.data(), (int)wName.size(),
wMsg.data(), (int)wMsg.size(),
m_computedRTL);
wchar_t prevCh = 0;
for (size_t i = 0; i < visual.size(); ++i)
{
__DrawCharacter(pFontTexture, visual[i], dwColor, prevCh);
prevCh = visual[i];
}
pFontTexture->UpdateTexture();
m_isUpdate = true;
return;
}
else
{
// Has tags (hyperlinks): Rebuild as "Message : Name" or "Name : Message"
// then use tag-aware rendering below
if (m_computedRTL)
{
// RTL: "Message : Name"
m_stText = m_chatMessage + " : " + m_chatName;
}
else
{
// LTR: "Name : Message" (original format)
m_stText = m_chatName + " : " + m_chatMessage;
}
// Re-convert to wide chars for tag-aware processing below
const char* utf8 = m_stText.c_str();
const int utf8Len = (int)m_stText.size();
wTextBuf.clear();
wTextBuf.resize((size_t)utf8Len + 1u, 0);
wTextLen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8, utf8Len, wTextBuf.data(), (int)wTextBuf.size());
if (wTextLen <= 0)
{
wTextLen = MultiByteToWideChar(CP_UTF8, 0, utf8, utf8Len, wTextBuf.data(), (int)wTextBuf.size());
if (wTextLen <= 0)
{
ResetState();
return;
}
}
// Fall through to tag-aware rendering below
}
}
// Check if text contains tags or RTL
const bool hasTags = (std::find(wTextBuf.begin(), wTextBuf.begin() + wTextLen, L'|') != (wTextBuf.begin() + wTextLen));
bool hasRTL = false;
for (int i = 0; i < wTextLen; ++i)
{
if (IsRTLCodepoint(wTextBuf[i]))
{
hasRTL = true;
break;
}
}
// Tag-aware BiDi rendering: Parse tags, apply BiDi per segment, track colors/hyperlinks
// OPTIMIZED: Use helper lambda to eliminate code duplication (was repeated 5+ times)
if (hasRTL || hasTags)
{
DWORD currentColor = dwColor;
int hyperlinkStep = 0; // 0=normal, 1=collecting metadata, 2=visible hyperlink
std::wstring hyperlinkMetadata;
static std::vector<wchar_t> s_currentSegment;
s_currentSegment.clear();
SHyperlink currentHyperlink;
currentHyperlink.sx = currentHyperlink.ex = 0;
// In chat RTL, force RTL base direction so prefixes like "[hyperlink]" don't flip the paragraph to LTR.
const bool forceRTLForBidi = (m_isChatMessage && m_computedRTL);
// OPTIMIZED: Single helper function for flushing segments (eliminates 5x code duplication)
auto FlushSegment = [&](DWORD segColor) -> int
{
if (s_currentSegment.empty())
return 0;
int totalWidth = 0;
// Apply BiDi transformation using optimized BuildVisualBidiText_Tagless
std::vector<wchar_t> visual = BuildVisualBidiText_Tagless(
s_currentSegment.data(), (int)s_currentSegment.size(), forceRTLForBidi);
wchar_t prevCh = m_pCharInfoVector.empty() ? 0 : 0; // no prev across segments
for (size_t j = 0; j < visual.size(); ++j)
{
int w = __DrawCharacter(pFontTexture, visual[j], segColor, prevCh);
totalWidth += w;
prevCh = visual[j];
}
s_currentSegment.clear();
return totalWidth;
};
// Prepend glyphs to the already-built draw list (used to place hyperlink before message in RTL chat).
auto PrependGlyphs = [&](CGraphicFontTexture* pFontTexture,
const std::vector<wchar_t>& chars,
DWORD color,
int& outWidth)
{
outWidth = 0;
static std::vector<CGraphicFontTexture::TCharacterInfomation*> s_newCharInfos;
static std::vector<DWORD> s_newColors;
static std::vector<float> s_newKerns;
s_newCharInfos.clear();
s_newColors.clear();
s_newKerns.clear();
s_newCharInfos.reserve(chars.size());
s_newColors.reserve(chars.size());
s_newKerns.reserve(chars.size());
for (size_t k = 0; k < chars.size(); ++k)
{
auto* pInfo = pFontTexture->GetCharacterInfomation(chars[k]);
if (!pInfo)
continue;
s_newCharInfos.push_back(pInfo);
s_newColors.push_back(color);
s_newKerns.push_back(0.0f);
outWidth += pInfo->advance;
m_textHeight = std::max((WORD)pInfo->height, m_textHeight);
}
m_pCharInfoVector.insert(m_pCharInfoVector.begin(), s_newCharInfos.begin(), s_newCharInfos.end());
m_dwColorInfoVector.insert(m_dwColorInfoVector.begin(), s_newColors.begin(), s_newColors.end());
m_kernVector.insert(m_kernVector.begin(), s_newKerns.begin(), s_newKerns.end());
for (auto& link : m_hyperlinkVector)
{
link.sx += outWidth;
link.ex += outWidth;
}
m_textWidth += outWidth;
};
// Parse text with tags
for (int i = 0; i < wTextLen;)
{
int tagLen = 0;
std::wstring tagExtra;
int tagType = GetTextTag(&wTextBuf[i], wTextLen - i, tagLen, tagExtra);
if (tagType == TEXT_TAG_COLOR)
{
// Flush current segment before changing color
currentHyperlink.ex += FlushSegment(currentColor);
currentColor = htoi(tagExtra.c_str(), 8);
i += tagLen;
}
else if (tagType == TEXT_TAG_RESTORE_COLOR)
{
// Flush segment before restoring color
currentHyperlink.ex += FlushSegment(currentColor);
currentColor = dwColor;
i += tagLen;
}
else if (tagType == TEXT_TAG_HYPERLINK_START)
{
hyperlinkStep = 1;
hyperlinkMetadata.clear();
i += tagLen;
}
else if (tagType == TEXT_TAG_HYPERLINK_END)
{
if (hyperlinkStep == 1)
{
// End of metadata, start visible section
// Flush any pending non-hyperlink segment first
currentHyperlink.ex += FlushSegment(currentColor);
hyperlinkStep = 2;
currentHyperlink.text = hyperlinkMetadata;
currentHyperlink.sx = currentHyperlink.ex; // Start hyperlink at current cursor position
}
else if (hyperlinkStep == 2)
{
// End of visible section - render hyperlink text with proper Arabic handling
// In RTL chat: we want the hyperlink chunk to appear BEFORE the message, even if logically appended.
if (!s_currentSegment.empty())
{
// OPTIMIZED: Use thread-local buffer for visible rendering
static std::vector<wchar_t> s_visibleToRender;
s_visibleToRender.clear();
// Find bracket positions: [ ... ]
int openBracket = -1, closeBracket = -1;
for (size_t idx = 0; idx < s_currentSegment.size(); ++idx)
{
if (s_currentSegment[idx] == L'[' && openBracket == -1)
openBracket = (int)idx;
else if (s_currentSegment[idx] == L']' && closeBracket == -1)
closeBracket = (int)idx;
}
if (openBracket >= 0 && closeBracket > openBracket)
{
// Keep '['
s_visibleToRender.push_back(L'[');
// Extract inside content and apply BiDi
static std::vector<wchar_t> s_content;
s_content.assign(
s_currentSegment.begin() + openBracket + 1,
s_currentSegment.begin() + closeBracket);
// FIX: Use false to let BiDi auto-detect direction from content
// This ensures English items like [Sword+9] stay LTR
// while Arabic items like [درع فولاذي+9] are properly RTL
std::vector<wchar_t> visual = BuildVisualBidiText_Tagless(
s_content.data(), (int)s_content.size(), false);
s_visibleToRender.insert(s_visibleToRender.end(), visual.begin(), visual.end());
// Keep ']'
s_visibleToRender.push_back(L']');
}
else
{
// No brackets: apply BiDi to whole segment
// FIX: Use false to let BiDi auto-detect direction from content
std::vector<wchar_t> visual = BuildVisualBidiText_Tagless(
s_currentSegment.data(), (int)s_currentSegment.size(), false);
s_visibleToRender.insert(s_visibleToRender.end(), visual.begin(), visual.end());
}
// Ensure a space AFTER the hyperlink chunk (so it becomes "[hyperlink] اختبار...")
s_visibleToRender.push_back(L' ');
// Key behavior:
// In RTL chat, place hyperlink BEFORE the message by prepending glyphs.
if (m_isChatMessage && m_computedRTL)
{
int addedWidth = 0;
PrependGlyphs(pFontTexture, s_visibleToRender, currentColor, addedWidth);
// Record the hyperlink range at the beginning (0..addedWidth)
currentHyperlink.sx = 0;
currentHyperlink.ex = addedWidth;
m_hyperlinkVector.push_back(currentHyperlink);
}
else
{
// LTR or non-chat: keep original "append" behavior
currentHyperlink.sx = currentHyperlink.ex;
wchar_t prevCh = 0;
for (size_t j = 0; j < s_visibleToRender.size(); ++j)
{
int w = __DrawCharacter(pFontTexture, s_visibleToRender[j], currentColor, prevCh);
currentHyperlink.ex += w;
prevCh = s_visibleToRender[j];
}
m_hyperlinkVector.push_back(currentHyperlink);
}
}
hyperlinkStep = 0;
s_currentSegment.clear();
}
i += tagLen;
}
else // TEXT_TAG_PLAIN or TEXT_TAG_TAG
{
if (hyperlinkStep == 1)
{
// Collecting hyperlink metadata (hidden)
hyperlinkMetadata.push_back(wTextBuf[i]);
}
else
{
// Add to current segment
// Will be BiDi-processed for normal text, or rendered directly for hyperlinks
s_currentSegment.push_back(wTextBuf[i]);
}
i += tagLen;
}
}
// Flush any remaining segment using optimized helper
currentHyperlink.ex += FlushSegment(currentColor);
pFontTexture->UpdateTexture();
m_isUpdate = true;
return;
}
// Simple LTR rendering for plain text (no tags, no RTL)
// Just draw characters in logical order
{
wchar_t prevCh = 0;
for (int i = 0; i < wTextLen; ++i)
{
__DrawCharacter(pFontTexture, wTextBuf[i], dwColor, prevCh);
prevCh = wTextBuf[i];
}
}
pFontTexture->UpdateTexture();
m_isUpdate = true;
}
void CGraphicTextInstance::Render(RECT * pClipRect)
{
if (!m_isUpdate)
return;
CGraphicText* pkText=m_roText.GetPointer();
if (!pkText)
return;
CGraphicFontTexture* pFontTexture = pkText->GetFontTexturePointer();
if (!pFontTexture)
return;
float fStanX = m_v3Position.x;
float fStanY = m_v3Position.y + 1.0f;
// Use the computed direction for this text instance, not the global UI direction
if (m_computedRTL)
{
switch (m_hAlign)
{
case HORIZONTAL_ALIGN_LEFT:
fStanX -= m_textWidth;
break;
case HORIZONTAL_ALIGN_CENTER:
fStanX -= float(m_textWidth / 2);
break;
}
}
else
{
switch (m_hAlign)
{
case HORIZONTAL_ALIGN_RIGHT:
fStanX -= m_textWidth;
break;
case HORIZONTAL_ALIGN_CENTER:
fStanX -= float(m_textWidth / 2);
break;
}
}
switch (m_vAlign)
{
case VERTICAL_ALIGN_BOTTOM:
fStanY -= m_textHeight;
break;
case VERTICAL_ALIGN_CENTER:
fStanY -= float(m_textHeight) / 2.0f;
break;
}
static std::unordered_map<LPDIRECT3DTEXTURE9, std::vector<SVertex>> s_outlineBatches;
static std::unordered_map<LPDIRECT3DTEXTURE9, std::vector<SVertex>> s_mainBatches;
s_outlineBatches.clear();
s_mainBatches.clear();
STATEMANAGER.SaveRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
STATEMANAGER.SaveRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
DWORD dwFogEnable = STATEMANAGER.GetRenderState(D3DRS_FOGENABLE);
DWORD dwLighting = STATEMANAGER.GetRenderState(D3DRS_LIGHTING);
STATEMANAGER.SetRenderState(D3DRS_FOGENABLE, FALSE);
STATEMANAGER.SetRenderState(D3DRS_LIGHTING, FALSE);
STATEMANAGER.SetFVF(D3DFVF_XYZ|D3DFVF_DIFFUSE|D3DFVF_TEX1);
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_MODULATE);
// LCD subpixel rendering: mask alpha writes to prevent corruption during two-pass blending
STATEMANAGER.SaveRenderState(D3DRS_COLORWRITEENABLE,
D3DCOLORWRITEENABLE_RED | D3DCOLORWRITEENABLE_GREEN | D3DCOLORWRITEENABLE_BLUE);
{
const float fFontHalfWeight=1.0f;
float fCurX;
float fCurY;
float fFontSx;
float fFontSy;
float fFontEx;
float fFontEy;
float fFontWidth;
float fFontHeight;
float fFontMaxHeight;
float fFontAdvance;
SVertex akVertex[4];
akVertex[0].z=m_v3Position.z;
akVertex[1].z=m_v3Position.z;
akVertex[2].z=m_v3Position.z;
akVertex[3].z=m_v3Position.z;
CGraphicFontTexture::TCharacterInfomation* pCurCharInfo;
if (m_isOutline)
{
fCurX=fStanX;
fCurY=fStanY;
fFontMaxHeight=0.0f;
int charIdx = 0;
CGraphicFontTexture::TPCharacterInfomationVector::iterator i;
for (i=m_pCharInfoVector.begin(); i!=m_pCharInfoVector.end(); ++i, ++charIdx)
{
pCurCharInfo = *i;
float fKern = (charIdx < (int)m_kernVector.size()) ? m_kernVector[charIdx] : 0.0f;
fCurX += fKern;
fFontWidth=float(pCurCharInfo->width);
fFontHeight=float(pCurCharInfo->height);
fFontMaxHeight=(std::max)(fFontMaxHeight, fFontHeight);
fFontAdvance=float(pCurCharInfo->advance);
if ((fCurX+fFontWidth)-m_v3Position.x > m_fLimitWidth) [[unlikely]] {
if (m_isMultiLine)
{
fCurX=fStanX;
fCurY+=fFontMaxHeight;
}
else
{
break;
}
}
if (pClipRect)
{
if (fCurY <= pClipRect->top)
{
fCurX += fFontAdvance;
continue;
}
}
fFontSx = fCurX + pCurCharInfo->bearingX - 0.5f;
fFontSy = fCurY - 0.5f;
fFontEx = fFontSx + fFontWidth;
fFontEy = fFontSy + fFontHeight;
pFontTexture->SelectTexture(pCurCharInfo->index);
std::vector<SVertex>& vtxBatch = s_outlineBatches[pFontTexture->GetD3DTexture()];
akVertex[0].u=pCurCharInfo->left;
akVertex[0].v=pCurCharInfo->top;
akVertex[1].u=pCurCharInfo->left;
akVertex[1].v=pCurCharInfo->bottom;
akVertex[2].u=pCurCharInfo->right;
akVertex[2].v=pCurCharInfo->top;
akVertex[3].u=pCurCharInfo->right;
akVertex[3].v=pCurCharInfo->bottom;
akVertex[3].color = akVertex[2].color = akVertex[1].color = akVertex[0].color = m_dwOutLineColor;
float feather = 0.0f; // m_fFontFeather
akVertex[0].y=fFontSy-feather;
akVertex[1].y=fFontEy+feather;
akVertex[2].y=fFontSy-feather;
akVertex[3].y=fFontEy+feather;
// 왼
akVertex[0].x=fFontSx-fFontHalfWeight-feather;
akVertex[1].x=fFontSx-fFontHalfWeight-feather;
akVertex[2].x=fFontEx-fFontHalfWeight+feather;
akVertex[3].x=fFontEx-fFontHalfWeight+feather;
vtxBatch.push_back(akVertex[0]); vtxBatch.push_back(akVertex[1]); vtxBatch.push_back(akVertex[2]);
vtxBatch.push_back(akVertex[2]); vtxBatch.push_back(akVertex[1]); vtxBatch.push_back(akVertex[3]);
// 오른
akVertex[0].x=fFontSx+fFontHalfWeight-feather;
akVertex[1].x=fFontSx+fFontHalfWeight-feather;
akVertex[2].x=fFontEx+fFontHalfWeight+feather;
akVertex[3].x=fFontEx+fFontHalfWeight+feather;
vtxBatch.push_back(akVertex[0]); vtxBatch.push_back(akVertex[1]); vtxBatch.push_back(akVertex[2]);
vtxBatch.push_back(akVertex[2]); vtxBatch.push_back(akVertex[1]); vtxBatch.push_back(akVertex[3]);
akVertex[0].x=fFontSx-feather;
akVertex[1].x=fFontSx-feather;
akVertex[2].x=fFontEx+feather;
akVertex[3].x=fFontEx+feather;
// 위
akVertex[0].y=fFontSy-fFontHalfWeight-feather;
akVertex[1].y=fFontEy-fFontHalfWeight+feather;
akVertex[2].y=fFontSy-fFontHalfWeight-feather;
akVertex[3].y=fFontEy-fFontHalfWeight+feather;
vtxBatch.push_back(akVertex[0]); vtxBatch.push_back(akVertex[1]); vtxBatch.push_back(akVertex[2]);
vtxBatch.push_back(akVertex[2]); vtxBatch.push_back(akVertex[1]); vtxBatch.push_back(akVertex[3]);
// 아래
akVertex[0].y=fFontSy+fFontHalfWeight-feather;
akVertex[1].y=fFontEy+fFontHalfWeight+feather;
akVertex[2].y=fFontSy+fFontHalfWeight-feather;
akVertex[3].y=fFontEy+fFontHalfWeight+feather;
vtxBatch.push_back(akVertex[0]); vtxBatch.push_back(akVertex[1]); vtxBatch.push_back(akVertex[2]);
vtxBatch.push_back(akVertex[2]); vtxBatch.push_back(akVertex[1]); vtxBatch.push_back(akVertex[3]);
fCurX += fFontAdvance;
}
}
fCurX=fStanX;
fCurY=fStanY;
fFontMaxHeight=0.0f;
for (int i = 0; i < (int)m_pCharInfoVector.size(); ++i)
{
pCurCharInfo = m_pCharInfoVector[i];
float fKern = (i < (int)m_kernVector.size()) ? m_kernVector[i] : 0.0f;
fCurX += fKern;
fFontWidth=float(pCurCharInfo->width);
fFontHeight=float(pCurCharInfo->height);
fFontMaxHeight=(std::max)(fFontMaxHeight, fFontHeight);
fFontAdvance=float(pCurCharInfo->advance);
if ((fCurX + fFontWidth) - m_v3Position.x > m_fLimitWidth) [[unlikely]] {
if (m_isMultiLine)
{
fCurX=fStanX;
fCurY+=fFontMaxHeight;
}
else
{
break;
}
}
if (pClipRect)
{
if (fCurY <= pClipRect->top)
{
fCurX += fFontAdvance;
continue;
}
}
fFontSx = fCurX + pCurCharInfo->bearingX - 0.5f;
fFontSy = fCurY-0.5f;
fFontEx = fFontSx + fFontWidth;
fFontEy = fFontSy + fFontHeight;
pFontTexture->SelectTexture(pCurCharInfo->index);
std::vector<SVertex>& vtxBatch = s_mainBatches[pFontTexture->GetD3DTexture()];
akVertex[0].x=fFontSx;
akVertex[0].y=fFontSy;
akVertex[0].u=pCurCharInfo->left;
akVertex[0].v=pCurCharInfo->top;
akVertex[1].x=fFontSx;
akVertex[1].y=fFontEy;
akVertex[1].u=pCurCharInfo->left;
akVertex[1].v=pCurCharInfo->bottom;
akVertex[2].x=fFontEx;
akVertex[2].y=fFontSy;
akVertex[2].u=pCurCharInfo->right;
akVertex[2].v=pCurCharInfo->top;
akVertex[3].x=fFontEx;
akVertex[3].y=fFontEy;
akVertex[3].u=pCurCharInfo->right;
akVertex[3].v=pCurCharInfo->bottom;
akVertex[0].color = akVertex[1].color = akVertex[2].color = akVertex[3].color = m_dwColorInfoVector[i];
vtxBatch.push_back(akVertex[0]); vtxBatch.push_back(akVertex[1]); vtxBatch.push_back(akVertex[2]);
vtxBatch.push_back(akVertex[2]); vtxBatch.push_back(akVertex[1]); vtxBatch.push_back(akVertex[3]);
fCurX += fFontAdvance;
}
}
// --- Selection background (Ctrl+A / shift-select) ---
// MR-15: Expose text selection highlighting to Python
{
// Determine selection range: IME state for active input fields, local state otherwise
int selBegin, selEnd;
if (m_isCursor && CIME::ms_bCaptureInput)
{
selBegin = CIME::GetSelBegin();
selEnd = CIME::GetSelEnd();
}
else
{
selBegin = m_selStart;
selEnd = m_selEnd;
}
// MR-15: -- END OF -- Expose text selection highlighting to Python
if (selBegin > selEnd) std::swap(selBegin, selEnd);
if (selBegin != selEnd)
{
float sx, sy, ex, ey;
// Convert logical selection positions to visual positions (handles tags)
int visualSelBegin = selBegin;
int visualSelEnd = selEnd;
if (!m_logicalToVisualPos.empty())
{
if (selBegin >= 0 && selBegin < (int)m_logicalToVisualPos.size())
visualSelBegin = m_logicalToVisualPos[selBegin];
if (selEnd >= 0 && selEnd < (int)m_logicalToVisualPos.size())
visualSelEnd = m_logicalToVisualPos[selEnd];
}
__GetTextPos(visualSelBegin, &sx, &sy);
__GetTextPos(visualSelEnd, &ex, &sy);
// Handle RTL - use the computed direction for this text instance
// MR-15: Expose text highlighting to Python
// Apply horizontal alignment (must match text rendering offset)
float alignOffset = 0.0f;
if (m_computedRTL)
{
switch (m_hAlign)
{
case HORIZONTAL_ALIGN_LEFT:
alignOffset = -(float)m_textWidth;
break;
case HORIZONTAL_ALIGN_CENTER:
alignOffset = -float(m_textWidth / 2);
break;
}
}
else
{
switch (m_hAlign)
{
case HORIZONTAL_ALIGN_RIGHT:
alignOffset = -(float)m_textWidth;
break;
case HORIZONTAL_ALIGN_CENTER:
alignOffset = -float(m_textWidth / 2);
break;
}
}
sx += m_v3Position.x + alignOffset;
ex += m_v3Position.x + alignOffset;
// MR-15: -- END OF -- Expose text highlighting to Python
// Apply vertical alignment
float top = m_v3Position.y;
float bot = m_v3Position.y + m_textHeight;
switch (m_vAlign)
{
case VERTICAL_ALIGN_BOTTOM:
top -= m_textHeight;
bot -= m_textHeight;
break;
case VERTICAL_ALIGN_CENTER:
top -= float(m_textHeight) / 2.0f;
bot -= float(m_textHeight) / 2.0f;
break;
}
TPDTVertex vertices[4];
vertices[0].diffuse = 0x80339CFF;
vertices[1].diffuse = 0x80339CFF;
vertices[2].diffuse = 0x80339CFF;
vertices[3].diffuse = 0x80339CFF;
vertices[0].position = TPosition(sx, top, 0.0f);
vertices[1].position = TPosition(ex, top, 0.0f);
vertices[2].position = TPosition(sx, bot, 0.0f);
vertices[3].position = TPosition(ex, bot, 0.0f);
STATEMANAGER.SetTexture(0, NULL);
CGraphicBase::SetDefaultIndexBuffer(CGraphicBase::DEFAULT_IB_FILL_RECT);
if (CGraphicBase::SetPDTStream(vertices, 4))
STATEMANAGER.DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 4, 0, 2);
}
}
// LCD subpixel two-pass rendering: correct per-channel alpha blending
auto DrawBatchLCD = [](const std::unordered_map<LPDIRECT3DTEXTURE9, std::vector<SVertex>>& batches, bool skipPass2) {
for (const auto& [pTexture, vtxBatch] : batches) {
if (vtxBatch.empty())
continue;
STATEMANAGER.SetTexture(0, pTexture);
// Pass 1: dest.rgb *= (1 - coverage.rgb)
STATEMANAGER.SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ZERO);
STATEMANAGER.SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCCOLOR);
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);
STATEMANAGER.DrawPrimitiveUP(D3DPT_TRIANGLELIST,
vtxBatch.size() / 3, vtxBatch.data(), sizeof(SVertex));
if (!skipPass2) {
// Pass 2: dest.rgb += textColor.rgb * coverage.rgb
STATEMANAGER.SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ONE);
STATEMANAGER.SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_MODULATE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE);
STATEMANAGER.DrawPrimitiveUP(D3DPT_TRIANGLELIST,
vtxBatch.size() / 3, vtxBatch.data(), sizeof(SVertex));
}
}
};
// Draw outline batches first (skip Pass 2 for black outlines — MODULATE with black = 0)
bool outlineIsBlack = ((m_dwOutLineColor & 0x00FFFFFF) == 0);
DrawBatchLCD(s_outlineBatches, outlineIsBlack);
// Draw main text batches (always both passes)
DrawBatchLCD(s_mainBatches, false);
if (m_isCursor)
{
// Draw Cursor
float sx, sy, ex, ey;
TDiffuse diffuse;
int curpos = CIME::GetCurPos();
int compend = curpos + CIME::GetCompLen();
// Convert logical cursor position to visual position (handles tags)
int visualCurpos = curpos;
int visualCompend = compend;
if (!m_logicalToVisualPos.empty())
{
if (curpos >= 0 && curpos < (int)m_logicalToVisualPos.size())
visualCurpos = m_logicalToVisualPos[curpos];
if (compend >= 0 && compend < (int)m_logicalToVisualPos.size())
visualCompend = m_logicalToVisualPos[compend];
}
__GetTextPos(visualCurpos, &sx, &sy);
// If Composition
if(visualCurpos<visualCompend)
{
diffuse = 0x7fffffff;
__GetTextPos(visualCompend, &ex, &sy);
}
else
{
diffuse = 0xffffffff;
ex = sx + 2;
}
// FOR_ARABIC_ALIGN
// Use the computed direction for this text instance, not the global UI direction
if (m_computedRTL)
{
sx += m_v3Position.x - m_textWidth;
ex += m_v3Position.x - m_textWidth;
sy += m_v3Position.y;
}
else
{
sx += m_v3Position.x;
sy += m_v3Position.y;
ex += m_v3Position.x;
}
// Apply vertical alignment adjustment BEFORE calculating ey
switch (m_vAlign)
{
case VERTICAL_ALIGN_BOTTOM:
sy -= m_textHeight;
break;
case VERTICAL_ALIGN_CENTER:
sy -= float(m_textHeight) / 2.0f;
break;
}
// NOW calculate ey after sy has been adjusted
ey = sy + m_textHeight;
TPDTVertex vertices[4];
vertices[0].diffuse = diffuse;
vertices[1].diffuse = diffuse;
vertices[2].diffuse = diffuse;
vertices[3].diffuse = diffuse;
vertices[0].position = TPosition(sx, sy, 0.0f);
vertices[1].position = TPosition(ex, sy, 0.0f);
vertices[2].position = TPosition(sx, ey, 0.0f);
vertices[3].position = TPosition(ex, ey, 0.0f);
STATEMANAGER.SetTexture(0, NULL);
CGraphicBase::SetDefaultIndexBuffer(CGraphicBase::DEFAULT_IB_FILL_RECT);
if (CGraphicBase::SetPDTStream(vertices, 4))
STATEMANAGER.DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 4, 0, 2);
int ulbegin = CIME::GetULBegin();
int ulend = CIME::GetULEnd();
if (ulbegin < ulend)
{
__GetTextPos(curpos+ulbegin, &sx, &sy);
__GetTextPos(curpos+ulend, &ex, &sy);
sx += m_v3Position.x;
sy += m_v3Position.y + m_textHeight;
ex += m_v3Position.x;
ey = sy + 2;
vertices[0].diffuse = 0xFFFF0000;
vertices[1].diffuse = 0xFFFF0000;
vertices[2].diffuse = 0xFFFF0000;
vertices[3].diffuse = 0xFFFF0000;
vertices[0].position = TPosition(sx, sy, 0.0f);
vertices[1].position = TPosition(ex, sy, 0.0f);
vertices[2].position = TPosition(sx, ey, 0.0f);
vertices[3].position = TPosition(ex, ey, 0.0f);
STATEMANAGER.DrawIndexedPrimitiveUP(D3DPT_TRIANGLELIST, 0, 4, 2, c_FillRectIndices, D3DFMT_INDEX16, vertices, sizeof(TPDTVertex));
}
}
STATEMANAGER.RestoreRenderState(D3DRS_COLORWRITEENABLE);
STATEMANAGER.RestoreRenderState(D3DRS_SRCBLEND);
STATEMANAGER.RestoreRenderState(D3DRS_DESTBLEND);
STATEMANAGER.SetRenderState(D3DRS_FOGENABLE, dwFogEnable);
STATEMANAGER.SetRenderState(D3DRS_LIGHTING, dwLighting);
if (m_hyperlinkVector.size() != 0)
{
// FOR_ARABIC_ALIGN: RTL text is drawn with offset (m_v3Position.x - m_textWidth)
// Use the computed direction for this text instance, not the global UI direction
int textOffsetX = m_computedRTL ? (int)(m_v3Position.x - m_textWidth) : (int)m_v3Position.x;
int lx = gs_mx - textOffsetX;
int ly = gs_my - (int)m_v3Position.y;
if (lx >= 0 && ly >= 0 && lx < m_textWidth && ly < m_textHeight)
{
std::vector<SHyperlink>::iterator it = m_hyperlinkVector.begin();
while (it != m_hyperlinkVector.end())
{
SHyperlink & link = *it++;
if (lx >= link.sx && lx < link.ex)
{
gs_hyperlinkText = link.text;
break;
}
}
}
}
}
void CGraphicTextInstance::CreateSystem(UINT uCapacity)
{
ms_kPool.Create(uCapacity);
}
void CGraphicTextInstance::DestroySystem()
{
ms_kPool.Destroy();
}
CGraphicTextInstance* CGraphicTextInstance::New()
{
return ms_kPool.Alloc();
}
void CGraphicTextInstance::Delete(CGraphicTextInstance* pkInst)
{
pkInst->Destroy();
ms_kPool.Free(pkInst);
}
void CGraphicTextInstance::ShowCursor()
{
m_isCursor = true;
}
void CGraphicTextInstance::HideCursor()
{
m_isCursor = false;
}
void CGraphicTextInstance::SetSelection(int iStart, int iEnd)
{
m_selStart = iStart;
m_selEnd = iEnd;
}
void CGraphicTextInstance::ClearSelection()
{
m_selStart = 0;
m_selEnd = 0;
}
void CGraphicTextInstance::ShowOutLine()
{
m_isOutline = true;
}
void CGraphicTextInstance::HideOutLine()
{
m_isOutline = false;
}
void CGraphicTextInstance::SetColor(DWORD color)
{
if (m_dwTextColor != color)
{
for (int i = 0; i < m_pCharInfoVector.size(); ++i)
if (m_dwColorInfoVector[i] == m_dwTextColor)
m_dwColorInfoVector[i] = color;
m_dwTextColor = color;
}
}
void CGraphicTextInstance::SetColor(float r, float g, float b, float a)
{
SetColor(D3DXCOLOR(r, g, b, a));
}
void CGraphicTextInstance::SetOutLineColor(DWORD color)
{
m_dwOutLineColor=color;
}
void CGraphicTextInstance::SetOutLineColor(float r, float g, float b, float a)
{
m_dwOutLineColor=D3DXCOLOR(r, g, b, a);
}
void CGraphicTextInstance::SetSecret(bool Value)
{
m_isSecret = Value;
}
void CGraphicTextInstance::SetOutline(bool Value)
{
m_isOutline = Value;
}
void CGraphicTextInstance::SetFeather(bool Value)
{
if (Value)
{
m_fFontFeather = c_fFontFeather;
}
else
{
m_fFontFeather = 0.0f;
}
}
void CGraphicTextInstance::SetMultiLine(bool Value)
{
m_isMultiLine = Value;
}
void CGraphicTextInstance::SetHorizonalAlign(int hAlign)
{
m_hAlign = hAlign;
}
void CGraphicTextInstance::SetVerticalAlign(int vAlign)
{
m_vAlign = vAlign;
}
void CGraphicTextInstance::SetMax(int iMax)
{
m_iMax = iMax;
}
void CGraphicTextInstance::SetLimitWidth(float fWidth)
{
m_fLimitWidth = fWidth;
}
void CGraphicTextInstance::SetValueString(const std::string& c_stValue)
{
if (0 == m_stText.compare(c_stValue))
return;
m_stText = c_stValue;
m_isUpdate = false;
}
void CGraphicTextInstance::SetValue(const char* c_szText, size_t len)
{
if (0 == m_stText.compare(c_szText))
return;
m_stText = c_szText;
m_isChatMessage = false; // Reset chat mode
m_isUpdate = false;
}
void CGraphicTextInstance::SetChatValue(const char* c_szName, const char* c_szMessage)
{
if (!c_szName || !c_szMessage)
return;
// Store separated components
m_chatName = c_szName;
m_chatMessage = c_szMessage;
m_isChatMessage = true;
// Build combined text for rendering (will be processed by Update())
// Use BuildVisualChatMessage in Update() instead of BuildVisualBidiText_Tagless
m_stText = std::string(c_szName) + " : " + std::string(c_szMessage);
m_isUpdate = false;
}
void CGraphicTextInstance::SetPosition(float fx, float fy, float fz)
{
m_v3Position.x = fx;
m_v3Position.y = fy;
m_v3Position.z = fz;
}
void CGraphicTextInstance::SetTextPointer(CGraphicText* pText)
{
m_roText = pText;
}
void CGraphicTextInstance::SetTextDirection(ETextDirection dir)
{
if (m_direction == dir)
return;
m_direction = dir;
m_isUpdate = false;
}
const std::string & CGraphicTextInstance::GetValueStringReference()
{
return m_stText;
}
WORD CGraphicTextInstance::GetTextLineCount()
{
CGraphicFontTexture::TCharacterInfomation* pCurCharInfo;
CGraphicFontTexture::TPCharacterInfomationVector::iterator itor;
float fx = 0.0f;
WORD wLineCount = 1;
for (itor=m_pCharInfoVector.begin(); itor!=m_pCharInfoVector.end(); ++itor)
{
pCurCharInfo = *itor;
float fFontWidth=float(pCurCharInfo->width);
float fFontAdvance=float(pCurCharInfo->advance);
//float fFontHeight=float(pCurCharInfo->height);
// NOTE : 폰트 출력에 Width 제한을 둡니다. - [levites]
if (fx+fFontWidth > m_fLimitWidth)
{
fx = 0.0f;
++wLineCount;
}
fx += fFontAdvance;
}
return wLineCount;
}
void CGraphicTextInstance::GetTextSize(int* pRetWidth, int* pRetHeight)
{
*pRetWidth = m_textWidth;
*pRetHeight = m_textHeight;
}
int CGraphicTextInstance::PixelPositionToCharacterPosition(int iPixelPosition)
{
// Clamp to valid range [0, textWidth]
int adjustedPixelPos = iPixelPosition;
if (adjustedPixelPos < 0)
adjustedPixelPos = 0;
if (adjustedPixelPos > m_textWidth)
adjustedPixelPos = m_textWidth;
// RTL: interpret click from right edge of rendered text
if (m_computedRTL)
adjustedPixelPos = m_textWidth - adjustedPixelPos;
int icurPosition = 0;
int visualPos = -1;
for (int i = 0; i < (int)m_pCharInfoVector.size(); ++i)
{
CGraphicFontTexture::TCharacterInfomation* pCurCharInfo = m_pCharInfoVector[i];
// Use advance instead of width (width is not reliable for cursor hit-testing)
int adv = pCurCharInfo->advance;
if (adv <= 0)
adv = pCurCharInfo->width;
int charStart = icurPosition;
icurPosition += adv;
int charMid = charStart + adv / 2;
if (adjustedPixelPos < charMid)
{
visualPos = i;
break;
}
}
if (visualPos < 0)
visualPos = (int)m_pCharInfoVector.size();
if (!m_visualToLogicalPos.empty() && visualPos >= 0 && visualPos < (int)m_visualToLogicalPos.size())
return m_visualToLogicalPos[visualPos];
return visualPos;
}
int CGraphicTextInstance::GetHorizontalAlign()
{
return m_hAlign;
}
void CGraphicTextInstance::__Initialize()
{
m_roText = NULL;
m_hAlign = HORIZONTAL_ALIGN_LEFT;
m_vAlign = VERTICAL_ALIGN_TOP;
m_iMax = 0;
m_fLimitWidth = 1600.0f;
m_isCursor = false;
m_isSecret = false;
m_isMultiLine = false;
m_selStart = 0;
m_selEnd = 0;
m_isOutline = false;
m_fFontFeather = c_fFontFeather;
m_isUpdate = false;
// Use Auto direction for input fields - they should auto-detect based on content
// Only chat messages should be explicitly set to RTL
m_direction = ETextDirection::Auto;
m_computedRTL = false;
m_isChatMessage = false;
m_chatName = "";
m_chatMessage = "";
m_textWidth = 0;
m_textHeight = 0;
m_v3Position.x = m_v3Position.y = m_v3Position.z = 0.0f;
m_dwOutLineColor=0xff000000;
}
void CGraphicTextInstance::Destroy()
{
m_stText="";
m_pCharInfoVector.clear();
m_kernVector.clear();
m_dwColorInfoVector.clear();
m_hyperlinkVector.clear();
m_logicalToVisualPos.clear();
m_visualToLogicalPos.clear();
__Initialize();
}
CGraphicTextInstance::CGraphicTextInstance()
{
__Initialize();
}
CGraphicTextInstance::~CGraphicTextInstance()
{
Destroy();
}