CText.cpp 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075
  1. #include "CText.h"
  2. #include <algorithm>
  3. #include <cstdio>
  4. #include <unordered_map>
  5. #include <filesystem>
  6. #include <sstream>
  7. #include <vector>
  8. #include <ft2build.h>
  9. #include FT_FREETYPE_H
  10. #include <glm/gtc/matrix_transform.hpp>
  11. #include <glm/gtc/type_ptr.hpp>
  12. #include "WallpaperEngine/Data/Model/DynamicValue.h"
  13. #include "WallpaperEngine/Data/Model/Material.h"
  14. #include "WallpaperEngine/Data/Model/Object.h"
  15. #include "WallpaperEngine/Data/Model/UserSetting.h"
  16. #include "WallpaperEngine/Logging/Log.h"
  17. #include "WallpaperEngine/Render/CFBO.h"
  18. #include "WallpaperEngine/Render/Camera.h"
  19. #include "WallpaperEngine/Render/TextureProvider.h"
  20. #include "WallpaperEngine/Render/Wallpapers/CScene.h"
  21. #include "WallpaperEngine/Scripting/ScriptEngine.h"
  22. using namespace WallpaperEngine::Render::Objects;
  23. using namespace WallpaperEngine::Render::Objects::Effects;
  24. namespace {
  25. // Text arrives as UTF-8 (scene JSON, user input, scripted values), but FreeType's FT_Load_Char
  26. // takes one Unicode codepoint per call - decode UTF-8 into codepoints first, or multi-byte
  27. // characters (CJK, emoji, accented Latin) get fed one raw byte at a time and rendered as garbage.
  28. // Malformed sequences are skipped byte-by-byte rather than aborting the whole string.
  29. std::vector<char32_t> decodeUtf8 (const std::string& text) {
  30. std::vector<char32_t> codepoints;
  31. size_t i = 0;
  32. while (i < text.size ()) {
  33. const auto lead = static_cast<unsigned char> (text[i]);
  34. size_t extraBytes;
  35. char32_t codepoint;
  36. if ((lead & 0x80) == 0x00) {
  37. codepoint = lead;
  38. extraBytes = 0;
  39. } else if ((lead & 0xE0) == 0xC0) {
  40. codepoint = lead & 0x1F;
  41. extraBytes = 1;
  42. } else if ((lead & 0xF0) == 0xE0) {
  43. codepoint = lead & 0x0F;
  44. extraBytes = 2;
  45. } else if ((lead & 0xF8) == 0xF0) {
  46. codepoint = lead & 0x07;
  47. extraBytes = 3;
  48. } else {
  49. // stray continuation byte or invalid lead byte - skip it and resync
  50. i++;
  51. continue;
  52. }
  53. if (i + extraBytes >= text.size ()) {
  54. // truncated multi-byte sequence at the end of the string
  55. break;
  56. }
  57. bool valid = true;
  58. for (size_t k = 1; k <= extraBytes; k++) {
  59. const auto cont = static_cast<unsigned char> (text[i + k]);
  60. if ((cont & 0xC0) != 0x80) {
  61. valid = false;
  62. break;
  63. }
  64. codepoint = (codepoint << 6) | (cont & 0x3F);
  65. }
  66. if (!valid) {
  67. i++;
  68. continue;
  69. }
  70. codepoints.push_back (codepoint);
  71. i += extraBytes + 1;
  72. }
  73. return codepoints;
  74. }
  75. // Fallback fonts, used only when the wallpaper's own font (loadEmbeddedFont) can't be loaded.
  76. const std::vector<std::string> kFontCandidates = {
  77. "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
  78. "/usr/share/fonts/TTF/DejaVuSans.ttf",
  79. "/usr/share/fonts/dejavu/DejaVuSans.ttf",
  80. "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
  81. };
  82. // WE's "systemfont_*" names are Windows fonts, ask fontconfig for the closest installed match
  83. std::string fontconfigMatch (const std::string& font) {
  84. static const std::vector<std::pair<std::string, std::string>> families = {
  85. { "systemfont_arial", "Arial,Liberation Sans,Arimo" },
  86. { "systemfont_calibri", "Calibri,Carlito" },
  87. { "systemfont_cambria", "Cambria,Caladea" },
  88. { "systemfont_comicsans", "Comic Sans MS,Comic Neue,Comic Relief" },
  89. { "systemfont_consolas", "Consolas,Inconsolata,DejaVu Sans Mono" },
  90. { "systemfont_sansserif", "sans-serif" },
  91. { "systemfont_segoe", "Segoe UI,Noto Sans,Open Sans" },
  92. { "systemfont_verdana", "Verdana,DejaVu Sans" },
  93. };
  94. std::string family = "sans-serif";
  95. for (const auto& [name, list] : families) {
  96. if (font == name) {
  97. family = list;
  98. break;
  99. }
  100. }
  101. const std::string command = "fc-match -f '%{file}' '" + family + "' 2>/dev/null";
  102. FILE* pipe = popen (command.c_str (), "r");
  103. if (pipe == nullptr) {
  104. return {};
  105. }
  106. std::string path;
  107. char buffer[512];
  108. while (fgets (buffer, sizeof (buffer), pipe) != nullptr) {
  109. path += buffer;
  110. }
  111. pclose (pipe);
  112. return !path.empty () && std::filesystem::exists (path) ? path : std::string ();
  113. }
  114. // Wraps the FreeType-rasterized glyph coverage bitmap (single R8 channel) as a
  115. // TextureProvider so it can be fed into the normal CRenderable/CPass pipeline.
  116. class TextGlyphTexture final : public WallpaperEngine::Render::TextureProvider {
  117. public:
  118. TextGlyphTexture () {
  119. glGenTextures (1, &m_textureID);
  120. glBindTexture (GL_TEXTURE_2D, m_textureID);
  121. glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  122. glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  123. glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  124. glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  125. }
  126. ~TextGlyphTexture () override { glDeleteTextures (1, &m_textureID); }
  127. void upload (int width, int height, const uint8_t* pixels) {
  128. m_width = static_cast<uint32_t> (width);
  129. m_height = static_cast<uint32_t> (height);
  130. m_resolution = glm::vec4 (
  131. static_cast<float> (m_width), static_cast<float> (m_height), static_cast<float> (m_width),
  132. static_cast<float> (m_height)
  133. );
  134. glBindTexture (GL_TEXTURE_2D, m_textureID);
  135. glPixelStorei (GL_UNPACK_ALIGNMENT, 1);
  136. glTexImage2D (GL_TEXTURE_2D, 0, GL_RED, width, height, 0, GL_RED, GL_UNSIGNED_BYTE, pixels);
  137. }
  138. [[nodiscard]] GLuint getTextureID (uint32_t) const override { return m_textureID; }
  139. [[nodiscard]] uint32_t getTextureWidth (uint32_t) const override { return m_width; }
  140. [[nodiscard]] uint32_t getTextureHeight (uint32_t) const override { return m_height; }
  141. [[nodiscard]] uint32_t getRealWidth () const override { return m_width; }
  142. [[nodiscard]] uint32_t getRealHeight () const override { return m_height; }
  143. [[nodiscard]] TextureFormat getFormat () const override { return TextureFormat_R8; }
  144. [[nodiscard]] uint32_t getFlags () const override { return TextureFlags_NoFlags; }
  145. [[nodiscard]] const std::vector<FrameSharedPtr>& getFrames () const override { return m_frames; }
  146. [[nodiscard]] const glm::vec4* getResolution () const override { return &m_resolution; }
  147. [[nodiscard]] bool isAnimated () const override { return false; }
  148. [[nodiscard]] uint32_t getSpritesheetCols () const override { return 1; }
  149. [[nodiscard]] uint32_t getSpritesheetRows () const override { return 1; }
  150. [[nodiscard]] uint32_t getSpritesheetFrames () const override { return 1; }
  151. [[nodiscard]] float getSpritesheetDuration () const override { return 0.0f; }
  152. void incrementUsageCount () const override { }
  153. void decrementUsageCount () const override { }
  154. void update () const override { }
  155. [[nodiscard]] bool isReady () const override { return m_width > 0 && m_height > 0; }
  156. private:
  157. std::vector<FrameSharedPtr> m_frames;
  158. glm::vec4 m_resolution = { 0.0f, 0.0f, 0.0f, 0.0f };
  159. uint32_t m_width = 0;
  160. uint32_t m_height = 0;
  161. GLuint m_textureID = GL_NONE;
  162. };
  163. // Base pass: tints the R8 glyph coverage texture with g_Color4, using WE's real "font" shader.
  164. // Normal (replace) blending so each frame fully overwrites the FBO's RGBA - CPass never clears
  165. // framebuffers between frames, so translucent blending would accumulate stale alpha over time.
  166. MaterialUniquePtr buildFontMaterial () {
  167. auto pass = std::make_unique<MaterialPass> (MaterialPass {
  168. .blending = BlendingMode_Normal,
  169. .cullmode = CullingMode_Disable,
  170. .depthtest = DepthtestMode_Disabled,
  171. .depthwrite = DepthwriteMode_Disabled,
  172. .shader = "font",
  173. .textures = {},
  174. .usertextures = {},
  175. .combos = { { "COLORFONT", 0 } },
  176. .constants = {},
  177. });
  178. auto material = std::make_unique<Material> ();
  179. material->filename = "<text/font>";
  180. material->passes.push_back (std::move (pass));
  181. return material;
  182. }
  183. // Final pass: blits the (possibly effect-processed) RGBA result onto the actual scene.
  184. MaterialUniquePtr buildCompositeMaterial () {
  185. auto pass = std::make_unique<MaterialPass> (MaterialPass {
  186. .blending = BlendingMode_Translucent,
  187. .cullmode = CullingMode_Disable,
  188. .depthtest = DepthtestMode_Disabled,
  189. .depthwrite = DepthwriteMode_Disabled,
  190. .shader = "genericimage",
  191. .textures = {},
  192. .usertextures = {},
  193. .combos = {},
  194. .constants = {},
  195. });
  196. auto material = std::make_unique<Material> ();
  197. material->filename = "<text/composite>";
  198. material->passes.push_back (std::move (pass));
  199. return material;
  200. }
  201. const Material& fontMaterial () {
  202. static const MaterialUniquePtr material = buildFontMaterial ();
  203. return *material;
  204. }
  205. const Material& compositeMaterial () {
  206. static const MaterialUniquePtr material = buildCompositeMaterial ();
  207. return *material;
  208. }
  209. // Mirrors CImage.cpp's clampParallaxAxis: keeps an edge pair from sliding past the viewport
  210. // once `offset` is added to both; a box too small to cover the viewport on this axis has no
  211. // ground to uncover and moves freely.
  212. float clampParallaxAxis (float offset, float edgeA, float edgeB, float sceneExtent) {
  213. const float low = std::min (edgeA, edgeB);
  214. const float high = std::max (edgeA, edgeB);
  215. const float half = sceneExtent / 2.0f;
  216. const float maxOffset = -half - low;
  217. const float minOffset = half - high;
  218. if (minOffset > maxOffset)
  219. return offset;
  220. return std::clamp (offset, minOffset, maxOffset);
  221. }
  222. } // namespace
  223. CText::CText (Wallpapers::CScene& scene, const Text& text) :
  224. CObject (scene, text), CRenderable (scene, text, fontMaterial ()), ScriptableObject (scene, text), m_text (text) {
  225. this->registerProperty ("color", *text.color->value);
  226. this->registerProperty ("alpha", *text.alpha->value);
  227. this->registerProperty ("origin", *text.origin->value);
  228. this->registerProperty ("scale", *text.scale->value);
  229. this->registerProperty ("visible", *text.visible->value);
  230. this->registerProperty ("pointSize", *text.pointSize->value);
  231. this->registerProperty ("text", *text.text->value);
  232. this->registerProperty ("parallaxDepth", *text.parallaxDepth->value);
  233. this->registerEffectConstants (text.effects);
  234. }
  235. CText::~CText () {
  236. if (m_layerHandle != Scripting::kInvalidLayerHandle) {
  237. this->getScene ().getScriptEngine ().destroyLayer (m_layerHandle);
  238. m_layerHandle = Scripting::kInvalidLayerHandle;
  239. }
  240. this->destroyPasses ();
  241. if (m_copySpacePosition != 0) {
  242. glDeleteBuffers (1, &m_copySpacePosition);
  243. }
  244. if (m_passSpacePosition != 0) {
  245. glDeleteBuffers (1, &m_passSpacePosition);
  246. }
  247. if (m_sceneSpacePosition != 0) {
  248. glDeleteBuffers (1, &m_sceneSpacePosition);
  249. }
  250. if (m_texcoordCopy != 0) {
  251. glDeleteBuffers (1, &m_texcoordCopy);
  252. }
  253. if (m_ftFace != nullptr) {
  254. FT_Done_Face (m_ftFace);
  255. }
  256. if (m_ftLibrary != nullptr) {
  257. FT_Done_FreeType (m_ftLibrary);
  258. }
  259. }
  260. void CText::destroyPasses () {
  261. for (auto* pass : m_passes) {
  262. delete pass;
  263. }
  264. m_passes.clear ();
  265. m_mainFBO = nullptr;
  266. m_subFBO = nullptr;
  267. m_currentMainFBO = nullptr;
  268. m_currentSubFBO = nullptr;
  269. }
  270. void CText::setup () {
  271. const bool scripted = m_text.text->value->getScriptSource ().has_value ();
  272. const auto& text = m_text.text->value->getString ();
  273. if (text.empty () && !scripted) {
  274. return;
  275. }
  276. if (!initFreeType ()) {
  277. return;
  278. }
  279. if (!loadEmbeddedFont () && !loadSystemFont ()) {
  280. return;
  281. }
  282. m_lastPixelSize = computeEffectivePixelSize ();
  283. FT_Set_Pixel_Sizes (m_ftFace, 0, static_cast<FT_UInt> (m_lastPixelSize));
  284. m_glyphTexture = std::make_shared<TextGlyphTexture> ();
  285. this->m_texture = m_glyphTexture;
  286. // Scripted text may have an empty placeholder; use a single space so the
  287. // glyph texture has non-zero dimensions until the script produces a value.
  288. rebuildTextureFrom (text.empty () ? std::string (" ") : text);
  289. if (scripted) {
  290. initScriptLayer ();
  291. }
  292. m_valid = m_glyphTexture != nullptr && m_glyphTexture->isReady ();
  293. if (!m_valid) {
  294. return;
  295. }
  296. CRenderable::setup ();
  297. buildPasses ();
  298. m_initialized = true;
  299. }
  300. bool CText::initFreeType () {
  301. if (FT_Init_FreeType (&m_ftLibrary) == 0) {
  302. return true;
  303. }
  304. sLog.error ("CText: FT_Init_FreeType failed for object ", m_text.name);
  305. return false;
  306. }
  307. bool CText::loadEmbeddedFont () {
  308. // Wallpapers packed in .pkg don't expose physical paths, so we read the font
  309. // into memory and use FT_New_Memory_Face. m_fontData must outlive the face.
  310. // `systemfont_*` references signal "use a system font"; let the fallback handle them.
  311. if (m_text.font.empty () || m_text.font.rfind ("systemfont_", 0) == 0) {
  312. return false;
  313. }
  314. try {
  315. auto stream = getAssetLocator ().read (m_text.font);
  316. stream->seekg (0, std::ios::end);
  317. const auto size = stream->tellg ();
  318. stream->seekg (0, std::ios::beg);
  319. m_fontData.resize (static_cast<size_t> (size));
  320. stream->read (reinterpret_cast<char*> (m_fontData.data ()), size);
  321. if (FT_New_Memory_Face (
  322. m_ftLibrary, m_fontData.data (), static_cast<FT_Long> (m_fontData.size ()), 0, &m_ftFace
  323. )
  324. == 0) {
  325. return true;
  326. }
  327. sLog.error ("CText: FT_New_Memory_Face failed for '", m_text.font, "', falling back to system font");
  328. } catch (const std::exception& e) {
  329. sLog.error ("CText: cannot read font '", m_text.font, "': ", e.what (), ", falling back to system font");
  330. }
  331. m_fontData.clear ();
  332. return false;
  333. }
  334. bool CText::loadSystemFont () {
  335. std::string fontPath = fontconfigMatch (m_text.font);
  336. if (fontPath.empty ()) {
  337. for (const auto& candidate : kFontCandidates) {
  338. if (std::filesystem::exists (candidate)) {
  339. fontPath = candidate;
  340. break;
  341. }
  342. }
  343. }
  344. if (fontPath.empty ()) {
  345. sLog.error ("CText: no usable system font found");
  346. return false;
  347. }
  348. if (FT_New_Face (m_ftLibrary, fontPath.c_str (), 0, &m_ftFace) != 0) {
  349. sLog.error ("CText: FT_New_Face failed for ", fontPath);
  350. return false;
  351. }
  352. return true;
  353. }
  354. unsigned int CText::computeEffectivePixelSize () const {
  355. // WE rasterizes glyphs at pointsize * 300 / 72 pixels (wallpaper64.exe), the object's scale only applies to the quad
  356. const float pointSize = std::clamp (m_text.pointSize->value->getFloat (), 1.0f, 256.0f);
  357. return std::max<unsigned int> (1u, static_cast<unsigned int> (std::lround (pointSize * 300.0f / 72.0f)));
  358. }
  359. void CText::initScriptLayer () {
  360. const auto& script = m_text.text->value->getScriptSource ();
  361. if (!script.has_value ()) {
  362. return;
  363. }
  364. // a script already running as a regular property module drives the value itself
  365. if (this->getScene ().getScriptEngine ().hasScript (*m_text.text->value)) {
  366. m_textFromProperty = true;
  367. return;
  368. }
  369. m_layerHandle = this->getScene ().getScriptEngine ().createLayerScript (
  370. *script, m_text.text->value->getProperties (), m_text.text->value->getString ()
  371. );
  372. if (m_layerHandle == Scripting::kInvalidLayerHandle) {
  373. sLog.error ("CText: createLayerScript failed for '", m_text.name, "'");
  374. }
  375. }
  376. void CText::rebuildTextureFrom (const std::string& text) {
  377. // like wallpaper64.exe, the layout box is made of whole font lines rather than the glyphs' ink
  378. FT_GlyphSlot slot = m_ftFace->glyph;
  379. const auto& faceMetrics = m_ftFace->size->metrics;
  380. const int ascender = std::max (1, static_cast<int> ((faceMetrics.ascender + 63) >> 6));
  381. const int descender = std::max (0, static_cast<int> ((-faceMetrics.descender + 63) >> 6));
  382. const int lineHeight = std::max (ascender + descender, static_cast<int> ((faceMetrics.height + 63) >> 6));
  383. std::unordered_map<char32_t, int> advances;
  384. const auto advanceOf = [&] (char32_t c) {
  385. const auto cached = advances.find (c);
  386. if (cached != advances.end ()) {
  387. return cached->second;
  388. }
  389. int advance = 0;
  390. if (FT_Load_Char (m_ftFace, static_cast<FT_ULong> (c), FT_LOAD_DEFAULT) == 0) {
  391. advance = static_cast<int> (slot->advance.x >> 6);
  392. }
  393. advances.emplace (c, advance);
  394. return advance;
  395. };
  396. const auto widthOf = [&] (const std::vector<char32_t>& codepoints) {
  397. int width = 0;
  398. for (const char32_t c : codepoints) {
  399. width += advanceOf (c);
  400. }
  401. return width;
  402. };
  403. const auto trimTrailingSpaces = [] (std::vector<char32_t>& codepoints) {
  404. while (!codepoints.empty () && (codepoints.back () == U' ' || codepoints.back () == U'\t')) {
  405. codepoints.pop_back ();
  406. }
  407. };
  408. const bool limitWidth = m_text.limitWidth->value->getBool ();
  409. const int maxLineWidth = std::max (1, static_cast<int> (m_text.maxWidth->value->getFloat ()));
  410. std::vector<std::vector<char32_t>> lines;
  411. size_t paragraphStart = 0;
  412. while (true) {
  413. const size_t pos = text.find ('\n', paragraphStart);
  414. std::string paragraph = text.substr (paragraphStart, pos == std::string::npos ? pos : pos - paragraphStart);
  415. if (!paragraph.empty () && paragraph.back () == '\r') {
  416. paragraph.pop_back ();
  417. }
  418. const auto codepoints = decodeUtf8 (paragraph);
  419. if (!limitWidth) {
  420. lines.push_back (codepoints);
  421. } else {
  422. std::vector<char32_t> current;
  423. int currentWidth = 0;
  424. const auto flush = [&] () {
  425. trimTrailingSpaces (current);
  426. lines.push_back (std::move (current));
  427. current.clear ();
  428. currentWidth = 0;
  429. };
  430. for (size_t i = 0; i < codepoints.size ();) {
  431. const bool space = codepoints[i] == U' ';
  432. size_t j = i;
  433. while (j < codepoints.size () && (codepoints[j] == U' ') == space) {
  434. j++;
  435. }
  436. const std::vector<char32_t> token (codepoints.begin () + static_cast<long> (i), codepoints.begin () + static_cast<long> (j));
  437. const int tokenWidth = widthOf (token);
  438. i = j;
  439. if (space) {
  440. // spaces at the start of a wrapped line are dropped
  441. if (!current.empty () || lines.empty ()) {
  442. current.insert (current.end (), token.begin (), token.end ());
  443. currentWidth += tokenWidth;
  444. }
  445. continue;
  446. }
  447. if (currentWidth + tokenWidth <= maxLineWidth) {
  448. current.insert (current.end (), token.begin (), token.end ());
  449. currentWidth += tokenWidth;
  450. continue;
  451. }
  452. if (!current.empty ()) {
  453. flush ();
  454. }
  455. if (tokenWidth <= maxLineWidth) {
  456. current = token;
  457. currentWidth = tokenWidth;
  458. continue;
  459. }
  460. // a single word wider than the box gets broken between characters
  461. for (const char32_t c : token) {
  462. const int advance = advanceOf (c);
  463. if (currentWidth + advance > maxLineWidth && !current.empty ()) {
  464. flush ();
  465. }
  466. current.push_back (c);
  467. currentWidth += advance;
  468. }
  469. }
  470. flush ();
  471. }
  472. if (pos == std::string::npos) {
  473. break;
  474. }
  475. paragraphStart = pos + 1;
  476. }
  477. if (m_text.limitRows->value->getBool ()) {
  478. const auto maxRows = static_cast<size_t> (std::max (1, static_cast<int> (std::lround (m_text.maxRows->value->getFloat ()))));
  479. if (lines.size () > maxRows) {
  480. lines.resize (maxRows);
  481. if (m_text.limitUseEllipsis->value->getBool ()) {
  482. auto& last = lines.back ();
  483. trimTrailingSpaces (last);
  484. std::vector<char32_t> ellipsis = { U'\u2026' };
  485. if (FT_Get_Char_Index (m_ftFace, U'\u2026') == 0) {
  486. ellipsis = { U'.', U'.', U'.' };
  487. }
  488. if (last.size () < ellipsis.size () || !std::equal (ellipsis.begin (), ellipsis.end (), last.end () - static_cast<long> (ellipsis.size ()))) {
  489. last.insert (last.end (), ellipsis.begin (), ellipsis.end ());
  490. }
  491. while (limitWidth && widthOf (last) > maxLineWidth && last.size () > ellipsis.size () + 1) {
  492. last.erase (last.end () - static_cast<long> (ellipsis.size ()) - 1);
  493. }
  494. }
  495. }
  496. }
  497. int maxWidth = 0;
  498. std::vector<int> lineWidths (lines.size ());
  499. for (size_t i = 0; i < lines.size (); ++i) {
  500. lineWidths[i] = widthOf (lines[i]);
  501. maxWidth = std::max (maxWidth, lineWidths[i]);
  502. }
  503. const int width = std::max (1, maxWidth);
  504. const int height = ascender + descender + static_cast<int> (lines.size () - 1) * lineHeight;
  505. const auto forEachGlyph = [&] (const auto& visit) {
  506. for (size_t i = 0; i < lines.size (); ++i) {
  507. int penX = 0;
  508. if (m_text.alignment == "center") {
  509. penX = (width - lineWidths[i]) / 2;
  510. } else if (m_text.alignment == "right") {
  511. penX = width - lineWidths[i];
  512. }
  513. const int baseline = ascender + static_cast<int> (i) * lineHeight;
  514. for (const char32_t c : lines[i]) {
  515. if (FT_Load_Char (m_ftFace, static_cast<FT_ULong> (c), FT_LOAD_RENDER) != 0) {
  516. continue;
  517. }
  518. visit (slot->bitmap, penX + slot->bitmap_left, baseline - slot->bitmap_top);
  519. penX += slot->advance.x >> 6;
  520. }
  521. }
  522. };
  523. // grow the texture for overhanging glyphs and leave room for effects that spread past them
  524. const int margin = std::max (16, static_cast<int> (m_lastPixelSize) / 6);
  525. int padLeft = margin;
  526. int padRight = margin;
  527. int padTop = margin;
  528. int padBottom = margin;
  529. forEachGlyph ([&] (const FT_Bitmap& bmp, int originX, int originY) {
  530. if (bmp.width == 0 || bmp.rows == 0) {
  531. return;
  532. }
  533. padLeft = std::max (padLeft, margin - originX);
  534. padRight = std::max (padRight, margin + originX + static_cast<int> (bmp.width) - width);
  535. padTop = std::max (padTop, margin - originY);
  536. padBottom = std::max (padBottom, margin + originY + static_cast<int> (bmp.rows) - height);
  537. });
  538. const int textureWidth = width + padLeft + padRight;
  539. const int textureHeight = height + padTop + padBottom;
  540. std::vector<uint8_t> pixels (static_cast<size_t> (textureWidth) * textureHeight, 0);
  541. forEachGlyph ([&] (const FT_Bitmap& bmp, int originX, int originY) {
  542. for (unsigned int row = 0; row < bmp.rows; ++row) {
  543. for (unsigned int col = 0; col < bmp.width; ++col) {
  544. const int dstX = padLeft + originX + static_cast<int> (col);
  545. const int dstY = padTop + originY + static_cast<int> (row);
  546. // glyphs may overlap by a pixel (kerning-less advances): keep the stronger coverage
  547. auto& dst = pixels[static_cast<size_t> (dstY) * textureWidth + dstX];
  548. dst = std::max (dst, bmp.buffer[row * bmp.pitch + col]);
  549. }
  550. }
  551. });
  552. // where the layout box sits relative to the texture's center, in texture pixels (y down)
  553. m_boxSize = { static_cast<float> (width), static_cast<float> (height) };
  554. m_boxShift = { static_cast<float> (padLeft - padRight) * 0.5f, static_cast<float> (padTop - padBottom) * 0.5f };
  555. m_descender = descender;
  556. const glm::ivec2 previousSize = m_textureSize;
  557. // FreeType bitmaps store row 0 as the glyph's TOP row; upload as-is and flip
  558. // when building the quad's V coordinates instead (see uploadQuadVertices).
  559. std::vector<uint8_t> flipped (pixels.size ());
  560. for (int row = 0; row < textureHeight; ++row) {
  561. std::copy_n (
  562. pixels.begin () + static_cast<long> (row) * textureWidth, textureWidth,
  563. flipped.begin () + static_cast<long> (textureHeight - 1 - row) * textureWidth
  564. );
  565. }
  566. static_cast<TextGlyphTexture*> (m_glyphTexture.get ())->upload (textureWidth, textureHeight, flipped.data ());
  567. m_textureSize = { textureWidth, textureHeight };
  568. m_quadSize = { static_cast<float> (textureWidth), static_cast<float> (textureHeight) };
  569. m_lastRenderedText = text;
  570. uploadQuadVertices ();
  571. // The glyph texture's own pixel size drives every FBO in the pass chain, so
  572. // resizing it (new text with a different width/height) means rebuilding them.
  573. if (m_initialized && previousSize != m_textureSize) {
  574. buildPasses ();
  575. }
  576. }
  577. void CText::uploadQuadVertices () {
  578. const float w = m_quadSize.x;
  579. const float h = m_quadSize.y;
  580. const float hx = w * 0.5f;
  581. const float hy = h * 0.5f;
  582. // "Copy space": local, unscaled, un-positioned quad spanning (0,0)-(w,h), matching
  583. // CImage's copy-space convention - used to render the glyph texture at its natural
  584. // pixel size into the first FBO, before any scene position/scale is applied.
  585. const GLfloat copySpacePosition[]
  586. = { 0.0f, h, 0.0f, 0.0f, 0.0f, 0.0f, w, h, 0.0f, w, h, 0.0f, 0.0f, 0.0f, 0.0f, w, 0.0f, 0.0f };
  587. const GLfloat passSpacePosition[]
  588. = { -1.0f, 1.0f, 0.0f, -1.0f, -1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, -1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f };
  589. // "Scene space": centered quad used for the final composite pass, positioned via
  590. // the scene MVP (translate + scale) computed in render().
  591. const GLfloat sceneSpacePosition[]
  592. = { -hx, -hy, 0.0f, -hx, hy, 0.0f, hx, -hy, 0.0f, hx, -hy, 0.0f, -hx, hy, 0.0f, hx, hy, 0.0f };
  593. const GLfloat texcoord[] = { 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f };
  594. if (m_copySpacePosition == 0) {
  595. glGenBuffers (1, &m_copySpacePosition);
  596. }
  597. glBindBuffer (GL_ARRAY_BUFFER, m_copySpacePosition);
  598. glBufferData (GL_ARRAY_BUFFER, sizeof (copySpacePosition), copySpacePosition, GL_STATIC_DRAW);
  599. if (m_passSpacePosition == 0) {
  600. glGenBuffers (1, &m_passSpacePosition);
  601. }
  602. glBindBuffer (GL_ARRAY_BUFFER, m_passSpacePosition);
  603. glBufferData (GL_ARRAY_BUFFER, sizeof (passSpacePosition), passSpacePosition, GL_STATIC_DRAW);
  604. if (m_sceneSpacePosition == 0) {
  605. glGenBuffers (1, &m_sceneSpacePosition);
  606. }
  607. glBindBuffer (GL_ARRAY_BUFFER, m_sceneSpacePosition);
  608. glBufferData (GL_ARRAY_BUFFER, sizeof (sceneSpacePosition), sceneSpacePosition, GL_STATIC_DRAW);
  609. if (m_texcoordCopy == 0) {
  610. glGenBuffers (1, &m_texcoordCopy);
  611. }
  612. glBindBuffer (GL_ARRAY_BUFFER, m_texcoordCopy);
  613. glBufferData (GL_ARRAY_BUFFER, sizeof (texcoord), texcoord, GL_STATIC_DRAW);
  614. m_modelViewProjectionCopy = glm::ortho<float> (0.0f, w, 0.0f, h);
  615. m_modelViewProjectionCopyInverse = glm::inverse (m_modelViewProjectionCopy);
  616. m_modelMatrix = m_modelViewProjectionCopy;
  617. m_viewProjectionMatrix = glm::mat4 (1.0f);
  618. }
  619. void CText::buildPasses () {
  620. this->destroyPasses ();
  621. if (m_quadSize.x <= 0.0f || m_quadSize.y <= 0.0f) {
  622. return;
  623. }
  624. const glm::vec2 fboSize = { std::max (1.0f, m_quadSize.x), std::max (1.0f, m_quadSize.y) };
  625. std::ostringstream nameA, nameB;
  626. nameA << "_rt_textComposite_" << this->getId () << "_a";
  627. nameB << "_rt_textComposite_" << this->getId () << "_b";
  628. this->m_currentMainFBO = this->m_mainFBO
  629. = this->create (nameA.str (), TextureFormat_ARGB8888, TextureFlags_ClampUVs, 1.0f, fboSize, fboSize);
  630. this->m_currentSubFBO = this->m_subFBO
  631. = this->create (nameB.str (), TextureFormat_ARGB8888, TextureFlags_ClampUVs, 1.0f, fboSize, fboSize);
  632. // base pass: tint the glyph coverage texture and render it into the first FBO
  633. for (const auto& pass : fontMaterial ().passes) {
  634. auto* cpass
  635. = new CPass (*this, std::make_shared<FBOProvider> (this), *pass, std::nullopt, std::nullopt, std::nullopt);
  636. cpass->setDestination (m_currentMainFBO);
  637. cpass->setInput (m_glyphTexture);
  638. cpass->setPosition (m_copySpacePosition);
  639. cpass->setTexCoord (m_texcoordCopy);
  640. cpass->setModelMatrix (&m_modelMatrix);
  641. cpass->setViewProjectionMatrix (&m_viewProjectionMatrix);
  642. cpass->setModelViewProjectionMatrix (&m_modelViewProjectionCopy);
  643. cpass->setModelViewProjectionMatrixInverse (&m_modelViewProjectionCopyInverse);
  644. m_passes.push_back (cpass);
  645. }
  646. std::shared_ptr<const TextureProvider> asInput = m_currentMainFBO;
  647. const auto& debug = this->getScene ().getContext ().getApp ().getContext ().settings.render.debug;
  648. if (!debug.baseOnly) {
  649. for (const auto& effect : m_text.effects) {
  650. if (!effect->visible->value->getBool ()) {
  651. continue;
  652. }
  653. const auto fboProvider = std::make_shared<FBOProvider> (this);
  654. for (const auto& fbo : effect->effect->fbos) {
  655. fboProvider->create (*fbo, TextureFlags_ClampUVs, fboSize);
  656. }
  657. auto curOverride = effect->passOverrides.begin ();
  658. const auto endOverride = effect->passOverrides.end ();
  659. // same target/previous bookkeeping as CImage::setupPasses
  660. bool inTargetSequence = false;
  661. std::shared_ptr<const TextureProvider> sequenceInput = nullptr;
  662. for (const auto& effectPass : effect->effect->passes) {
  663. if (!effectPass->material.has_value ()) {
  664. // command-only passes (e.g. plain FBO copies) aren't supported for text
  665. continue;
  666. }
  667. for (auto& matPass : effectPass->material.value ()->passes) {
  668. const auto override = curOverride != endOverride
  669. ? **curOverride
  670. : std::optional<std::reference_wrapper<const ImageEffectPassOverride>> (std::nullopt);
  671. const auto target = effectPass->target.has_value ()
  672. ? *effectPass->target
  673. : std::optional<std::reference_wrapper<std::string>> (std::nullopt);
  674. auto* cpass = new CPass (*this, fboProvider, *matPass, override, effectPass->binds, target);
  675. std::shared_ptr<const CFBO> drawTo = this->m_currentSubFBO;
  676. bool writesToTarget = false;
  677. if (target.has_value ()) {
  678. std::shared_ptr<const CFBO> resolved = fboProvider->find (target->get ());
  679. if (resolved == nullptr) {
  680. resolved = this->getScene ().findFBO (target->get ());
  681. }
  682. if (resolved != nullptr) {
  683. if (!inTargetSequence) {
  684. sequenceInput = asInput;
  685. inTargetSequence = true;
  686. }
  687. drawTo = resolved;
  688. writesToTarget = true;
  689. } else {
  690. sLog.error ("Text pass target FBO '", target->get (), "' could not be resolved for ", m_text.name);
  691. }
  692. }
  693. cpass->setDestination (drawTo);
  694. cpass->setInput (asInput);
  695. cpass->setPreviousInput (inTargetSequence ? sequenceInput : nullptr);
  696. cpass->setPosition (m_passSpacePosition);
  697. cpass->setTexCoord (m_texcoordCopy);
  698. cpass->setModelMatrix (&m_modelMatrix);
  699. cpass->setViewProjectionMatrix (&m_viewProjectionMatrix);
  700. cpass->setModelViewProjectionMatrix (&m_modelViewProjectionPass);
  701. cpass->setModelViewProjectionMatrixInverse (&m_modelViewProjectionPass);
  702. m_passes.push_back (cpass);
  703. asInput = drawTo;
  704. if (!writesToTarget) {
  705. std::swap (this->m_currentMainFBO, this->m_currentSubFBO);
  706. inTargetSequence = false;
  707. sequenceInput = nullptr;
  708. }
  709. }
  710. if (curOverride != endOverride) {
  711. ++curOverride;
  712. }
  713. }
  714. }
  715. }
  716. // final pass: composite the accumulated result onto the actual scene
  717. for (const auto& pass : compositeMaterial ().passes) {
  718. auto* cpass
  719. = new CPass (*this, std::make_shared<FBOProvider> (this), *pass, std::nullopt, std::nullopt, std::nullopt);
  720. cpass->setDestination (this->getScene ().getFBO ());
  721. cpass->setInput (asInput);
  722. cpass->setPosition (m_sceneSpacePosition);
  723. cpass->setTexCoord (m_texcoordCopy);
  724. cpass->setModelMatrix (&m_modelMatrix);
  725. cpass->setViewProjectionMatrix (&m_viewProjectionMatrix);
  726. cpass->setModelViewProjectionMatrix (&m_modelViewProjectionScreen);
  727. cpass->setModelViewProjectionMatrixInverse (&m_modelViewProjectionScreenInverse);
  728. m_passes.push_back (cpass);
  729. }
  730. }
  731. void CText::render () {
  732. if (!m_initialized) {
  733. return;
  734. }
  735. const auto& appContext = this->getScene ().getContext ().getApp ().getContext ();
  736. const auto visibility = appContext.resolveObjectVisibility (this->getId (), this->getObject ().name);
  737. if (!visibility.value_or (m_text.visible->value->getBool ())) {
  738. return;
  739. }
  740. std::string renderedText = m_lastRenderedText;
  741. if (m_layerHandle != Scripting::kInvalidLayerHandle) {
  742. auto& se = this->getScene ().getScriptEngine ();
  743. se.tickLayer (
  744. m_layerHandle, static_cast<double> (getScene ().getTime ()),
  745. static_cast<double> (getScene ().getDeltaTime ()), static_cast<double> (getScene ().getFps ())
  746. );
  747. const std::string current = se.layerText (m_layerHandle);
  748. renderedText = current.empty () ? std::string (" ") : current;
  749. } else if (m_textFromProperty) {
  750. const std::string current = m_text.text->value->getString ();
  751. renderedText = current.empty () ? std::string (" ") : current;
  752. }
  753. const unsigned int pixelSize = computeEffectivePixelSize ();
  754. if (pixelSize != m_lastPixelSize) {
  755. m_lastPixelSize = pixelSize;
  756. FT_Set_Pixel_Sizes (m_ftFace, 0, static_cast<FT_UInt> (m_lastPixelSize));
  757. rebuildTextureFrom (renderedText);
  758. } else if (renderedText != m_lastRenderedText) {
  759. rebuildTextureFrom (renderedText);
  760. }
  761. glm::vec3 scale = m_text.scale->value->getVec3 ();
  762. glm::vec3 origin = m_text.origin->value->getVec3 ();
  763. // texts sit under group/locator objects, same as CImage::resolveTransform
  764. if (m_text.parent.has_value ()) {
  765. std::vector<const Object*> ancestors;
  766. for (const Object* current = &m_text; current->parent.has_value () && ancestors.size () < 32;) {
  767. const auto* parentObject = this->getScene ().getObject (current->parent.value ());
  768. if (parentObject == nullptr) {
  769. break;
  770. }
  771. current = &parentObject->getObject ();
  772. ancestors.push_back (current);
  773. }
  774. glm::vec3 parentOrigin (0.0f);
  775. glm::vec3 parentScale (1.0f);
  776. float parentAngle = 0.0f;
  777. const auto rotate = [] (const glm::vec2& v, float angle) {
  778. const float cosine = std::cos (angle);
  779. const float sine = std::sin (angle);
  780. return glm::vec2 (v.x * cosine - v.y * sine, v.x * sine + v.y * cosine);
  781. };
  782. for (auto it = ancestors.rbegin (); it != ancestors.rend (); ++it) {
  783. const Object& node = **it;
  784. glm::vec3 nodeOrigin = node.origin->value->getVec3 ();
  785. glm::vec3 nodeScale = node.groupScale->value->getVec3 ();
  786. float nodeAngle = node.groupAngles->value->getVec3 ().z;
  787. if (node.is<Image> ()) {
  788. nodeScale = node.as<Image> ()->scale->value->getVec3 ();
  789. nodeAngle = node.as<Image> ()->angles->value->getVec3 ().z;
  790. }
  791. const glm::vec2 offset = rotate ({ nodeOrigin.x * parentScale.x, nodeOrigin.y * parentScale.y }, parentAngle);
  792. parentOrigin = { parentOrigin.x + offset.x, parentOrigin.y + offset.y,
  793. parentOrigin.z + nodeOrigin.z * parentScale.z };
  794. parentScale *= nodeScale;
  795. parentAngle += nodeAngle;
  796. }
  797. const glm::vec2 offset = rotate ({ origin.x * parentScale.x, origin.y * parentScale.y }, parentAngle);
  798. origin = { parentOrigin.x + offset.x, parentOrigin.y + offset.y, parentOrigin.z + origin.z * parentScale.z };
  799. scale *= parentScale;
  800. }
  801. // the glyph bbox is centered on the origin then shifted by the alignment anchor (wallpaper64.exe), json "size" is not used
  802. const float scaledHalfWidth = m_boxSize.x * 0.5f * scale.x;
  803. const float scaledHalfHeight = m_boxSize.y * 0.5f * scale.y;
  804. float offsetX = 0.0f;
  805. if (m_text.alignment == "left") {
  806. offsetX = scaledHalfWidth;
  807. } else if (m_text.alignment == "right") {
  808. offsetX = -scaledHalfWidth;
  809. }
  810. // offsetY is added to origin.y, which grows towards the top of the screen
  811. float offsetY = 0.0f;
  812. if (m_text.verticalalign == "top") {
  813. offsetY = -scaledHalfHeight;
  814. } else if (m_text.verticalalign == "bottom") {
  815. offsetY = scaledHalfHeight;
  816. } else {
  817. // "center" is moved down by half the descender, matching WE
  818. offsetY = -static_cast<float> (m_descender) * 0.5f * scale.y;
  819. }
  820. // the texture center is off the box center by the overhang padding, move the quad the opposite way
  821. offsetX -= m_boxShift.x * scale.x;
  822. offsetY += m_boxShift.y * scale.y;
  823. // WE uses a Y-down coordinate system; match CImage's convention (CImage.cpp's
  824. // updateScenePosition) of scene_h/2 - y rather than y - scene_h/2.
  825. const float scene_w = getScene ().getCamera ().getWidth ();
  826. const float scene_h = getScene ().getCamera ().getHeight ();
  827. // Matches CImage's parallax handling (CImage.cpp:updateScreenSpacePosition) in the same
  828. // pre-scale, canvas-space units as origin, so text stays visually locked to other objects at
  829. // the same parallaxDepth. Added directly to gl_origin (not after the model matrix) so it
  830. // isn't inadvertently multiplied by this object's own "scale".
  831. glm::vec2 parallaxOffset = { 0.0f, 0.0f };
  832. // CScene::renderFrame() already folds disableparallax into getParallaxDisplacement()
  833. if (this->getScene ().getScene ().camera.parallax.enabled->value->getBool ()) {
  834. parallaxOffset = this->getScene ().getParallaxOffset (m_text);
  835. // mirrors CImage's parallax clamp, or a text layer drifts past its edges while a same-depth
  836. // CImage backing panel freezes, visibly separating the two
  837. if (this->getScene ().getContext ().getApp ().getContext ().settings.mouse.clampParallaxToImageSize) {
  838. const float baseX = origin.x + offsetX - scene_w * 0.5f;
  839. const float baseY = scene_h * 0.5f - (origin.y + offsetY);
  840. parallaxOffset.x = clampParallaxAxis (
  841. parallaxOffset.x, baseX - scaledHalfWidth, baseX + scaledHalfWidth, getScene ().getCanvasWidth ()
  842. );
  843. parallaxOffset.y = clampParallaxAxis (
  844. parallaxOffset.y, baseY - scaledHalfHeight, baseY + scaledHalfHeight, getScene ().getCanvasHeight ()
  845. );
  846. }
  847. }
  848. const glm::vec3 gl_origin = {
  849. origin.x + offsetX - scene_w * 0.5f + parallaxOffset.x,
  850. scene_h * 0.5f - (origin.y + offsetY) + parallaxOffset.y,
  851. origin.z,
  852. };
  853. glm::mat4 model = glm::translate (glm::mat4 (1.0f), gl_origin);
  854. model = glm::scale (model, scale);
  855. m_modelViewProjectionScreen = getScene ().getCamera ().getProjection () * getScene ().getCamera ().getLookAt () * model;
  856. m_modelViewProjectionScreenInverse = glm::inverse (m_modelViewProjectionScreen);
  857. glColorMask (true, true, true, true);
  858. glDisable (GL_DEPTH_TEST);
  859. #if !NDEBUG
  860. std::string str = "Text " + this->getObject ().name + " (" + std::to_string (this->getId ()) + ")";
  861. glPushDebugGroup (GL_DEBUG_SOURCE_APPLICATION, 0, -1, str.c_str ());
  862. #endif /* DEBUG */
  863. for (auto* pass : m_passes) {
  864. pass->render ();
  865. }
  866. #if !NDEBUG
  867. glPopDebugGroup ();
  868. #endif /* DEBUG */
  869. }
  870. const float& CText::getBrightness () const {
  871. static constexpr float kUnitBrightness = 1.0f;
  872. return kUnitBrightness;
  873. }
  874. const float& CText::getUserAlpha () const { return m_text.alpha->value->getFloat (); }
  875. const float& CText::getAlpha () const { return m_text.alpha->value->getFloat (); }
  876. const glm::vec3& CText::getColor () const { return m_text.color->value->getVec3 (); }
  877. const glm::vec4& CText::getColor4 () const {
  878. const glm::vec3 rgb = m_text.color->value->getVec3 ();
  879. m_color4Cache = glm::vec4 (rgb, m_text.alpha->value->getFloat ());
  880. return m_color4Cache;
  881. }
  882. const glm::vec3& CText::getCompositeColor () const { return m_text.color->value->getVec3 (); }