Files
godot-website/article/complex-text-layouts-progress-report-2/index.html
2024-06-06 07:38:13 +00:00

29 lines
26 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html><html lang=en><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><meta name=author content="Godot Engine"><meta name=description content="Report on the complex text layouts support implementation progress, including changes to Godot's Font resources, and UI mirroring and BiDi implementation details."><meta name=theme-color content="#3d8fcc"><meta property="og:site_name" content="Godot Engine"><meta property="og:url" content="https://godotengine.org/article/complex-text-layouts-progress-report-2/"><meta name=twitter:site content="@godotengine"><meta property="og:title" content="Complex text layouts progress report #2"><meta property="og:description" content="Report on the complex text layouts support implementation progress, including changes to Godot's Font resources, and UI mirroring and BiDi implementation details."><meta property="og:image" content="https://godotengine.org/storage/app/uploads/public/5f9/ab1/122/5f9ab11220775871830624.png"><meta property="og:type" content="article"><meta name=twitter:card content="summary_large_image"><title>Complex text layouts progress report #2</title><link rel=alternate type=application/rss+xml title="Godot News" href=/rss.xml><link rel=icon href=/assets/favicon.png sizes=any><link rel=icon href=/assets/favicon.svg type=image/svg+xml><link rel=stylesheet href=/assets/css/main.css?112><link rel=stylesheet href=/assets/css/tobii.min.css><link rel=preload as=font href=/assets/fonts/Montserrat-Bold.woff2 crossorigin><link rel=preload as=font href=/assets/fonts/Montserrat-ExtraBold.woff2 crossorigin><link rel=me href=https://mastodon.gamedev.place/@godotengine><input type=checkbox id=nav_toggle_cb><header><div class="container flex align-center"><div id=nav_head><a href=/ id=logo-link><img class=nav-logo src=/assets/logo.svg width=136 height=48 alt="Godot Engine">
<img class="nav-logo dark-logo" src=/assets/logo_dark.svg width=136 height=48 alt="Godot Engine"></a>
<label for=nav_toggle_cb id=nav_toggle_btn><img src=/assets/icons/hamburger.svg width=24 height=24 alt="Main menu"></label></div><nav id=nav><ul class=left><li><a href=/features/>Features</a><li class=only-on-mobile><a href=/showcase/>Showcase</a><li><a href=/blog/>Blog</a><li><a href=/community/>Community</a><li><a href=/contact/>About</a><li><a href=https://godotengine.org/asset-library/asset>Assets</a></ul><ul class=right><li><a href=/download/windows/ class=set-os-download-url>Download</a><li><a href=https://docs.godotengine.org>Docs</a><li><a href=https://docs.godotengine.org/en/stable/community/contributing/index.html>Contribute</a><li class=fund><a href=https://fund.godotengine.org>&#xFE0E; Donate</a></ul></nav></div></header><main><style>body{background-color:var(--background-color)}h1{margin-bottom:8px;margin-top:32px}:not(pre)>code{background:var(--code-background-color);padding:1px 4px;font-size:.95em;border-radius:3px}pre{background:var(--codeblock-background-color);color:var(--codeblock-color)}pre code{display:block;overflow-x:auto;padding:.5em}.date-big{line-height:2;margin-left:32px}article{background-color:var(--base-color);box-shadow:0 3px 2px rgba(0,0,0,.15)}figure{margin:0}figure img{margin:0}article img,article video{max-width:100%;height:auto;display:block;margin:auto;margin-top:16px;margin-bottom:16px}article h1{margin-top:64px}article h2,article h3,article h4{margin-top:42px}.article-info{display:flex;flex-direction:column;gap:8px}.article-metadata{display:flex;gap:24px;align-items:center;font-family:var(--header-font-family);margin-bottom:12px}@media(max-width:900px){.article-metadata{flex-direction:column;align-items:flex-start;gap:16px}}.article-author{color:var(--base-color-text-subtitle-date);font-weight:700;font-size:18px;flex-grow:1;display:flex;gap:12px;align-items:center}.article-author .avatar{border-radius:100%;margin:0}.article-author .by{color:var(--base-color-text-subtitle)}.article-metadata .date{color:var(--base-color-text-subtitle-date)}.article-metadata .date.post-recent-highlight{color:var(--post-recent-highlight-color);opacity:.8}.article-metadata .date.post-recent-highlight::after{font-size:80%;content:"NEW";border:2px solid var(--post-recent-highlight-color);padding:2px 3px;margin-left:8px}.tag.active{filter:saturate(.75)}@media screen and (min-width:900px){article .content{width:70%;margin:auto}}@media(max-width:900px){body{background-color:var(--base-color)}article{background-color:initial;box-shadow:none}article img:first-child,article video:first-child{max-width:100%}}</style><link rel=stylesheet href=/assets/css/highlight.obsidian.min.css><div class=container><article class=padded><div class="content article-container"><figure class=article-cover><img src=/storage/app/uploads/public/5f9/ab1/122/5f9ab11220775871830624.png title alt=" " class=rounded-lg style=width:100%;height:auto;background-color:initial></figure><div class=article-info><h1>Complex text layouts progress report #2</h1><div class=article-metadata><div class=article-author><span>By:</span>
<img class=avatar width=25 height=25 src=/assets/images/authors/default_avatar.svg alt="Pāvels Nadtočajevs" loading=lazy>
<span class=by>Pāvels Nadtočajevs</span></div><span class=date data-post-date="2020-11-10 08:00:00 +0000">10 November 2020</span></div><div class=tags><a href=/blog/progress-report><div class="tag active">Progress Report</div></a></div></div><div class="card card-warning"><p>This article is from <strong>November 2020</strong>, some of its contents might be outdated and no longer accurate.<br>You can find up-to-date information about the engine in the <a href=https://docs.godotengine.org/en/stable/>official documentation</a>.</div><div class=article-body><h1 id=introduction>Introduction</h1><p>This is the second part of my work on Complex Text Layouts for Godot 4.0, focusing on Fonts and UI mirroring.<p>See <a href=https://github.com/godotengine/godot-proposals/issues/1180>godot-proposals#1180</a>, <a href=https://github.com/godotengine/godot-proposals/issues/1181>godot-proposals#1181</a>, <a href=https://github.com/godotengine/godot-proposals/issues/1182>godot-proposals#1182</a>, and <a href=https://github.com/godotengine/godot-proposals/issues/1183>godot-proposals#1183</a> on GitHub for detailed information on <abbr title="Complex Text Layouts">CTL</abbr> proposals and feedback.<p>See also the <a href=https://godotengine.org/article/complex-text-layouts-progress-report-1>previous progress report</a> for the <code class="language-plaintext highlighter-rouge">TextServer</code> API implementation details.<h1 id=changes-to-the-godot-fonts>Changes to the Godot Fonts</h1><p>Since font handling was moved to <code class="language-plaintext highlighter-rouge">TextServer</code>, some substantial changes were made to the Godot <code class="language-plaintext highlighter-rouge">Font</code> and related classes:<p><code class="language-plaintext highlighter-rouge">BitmapFont</code>, <code class="language-plaintext highlighter-rouge">DynamicFont</code> and <code class="language-plaintext highlighter-rouge">DynamicFontData</code> were removed and replaced with universal <code class="language-plaintext highlighter-rouge">Font</code> and <code class="language-plaintext highlighter-rouge">FontData</code> resources which are backed by <code class="language-plaintext highlighter-rouge">TextServer</code>. This provides cleaner font fallback/substitution and possibility for custom text servers to expose different types of fonts without interface changes, for example direct access to the system fonts.<p>The new <code class="language-plaintext highlighter-rouge">Font</code> class provides new functions to draw and measure multiline text, and apply alignment.<p>Font and outline size, that were properties of the <code class="language-plaintext highlighter-rouge">Font</code> instance, are moved to the arguments of the draw functions and theme constants. This allows changing font size for individual draw calls, controls, or spans of text in the <code class="language-plaintext highlighter-rouge">RichTextLabel</code> control without creating new <code class="language-plaintext highlighter-rouge">Font</code> instances.<p>Functions for loading font data from the memory buffer are exposed to GDScript and GDNative.<h2 id=font-substitution-system-for-scripts-and-languages>Font substitution system for scripts and languages</h2><p>A new, smarter font substitution system is added:<ul><li>Each <code class="language-plaintext highlighter-rouge">FontData</code> has an associated list of supported scripts (writing systems) and languages. For a TrueType/OpenType fonts, a script list is populated automatically from the OS2 table, but its not always precise.<li>Script and language support can be overridden by the user.<li>For each run of text with a specific script and language, <code class="language-plaintext highlighter-rouge">TextServer</code> will try to use fonts in the following order:<ul><li>Fonts with both script and language supported.<li>Fonts with script supported.<li>Rest of the fonts.</ul></ul><p>Herere a few cases of manual override:<ol><li>Many Latin fonts have a limited set of Greek characters for use in scientific texts (and such fonts usually have Greek script support flag set in the OS2 table), but its not always enough to display Greek text. Adding a separate font with full Greek support, and disabling Greek support in the main font will prevent <code class="language-plaintext highlighter-rouge">TextServer</code> from mixing characters form different fonts.<li>TrueType/OpenType font tables do not have flags for rare/ancient scripts (e.g. Egyptian hieroglyphs), enabling script support manually will speed up font substitution.<li>Some languages use the same script, but different font styles (e.g. Kufic, Naskh and Nastaʼlīq writing styles preferred for writing different Arabic languages; or the use of traditional Chinese characters in different regions and CJK variants - Traditional Chinese, Simplified Chinese, Japanese, and Korean). Setting language overrides allows to seamlessly use the same font stack for the text in different languages and get the desired style.</ol><p><em>Incorrect font used for “μ”:</em> <img src=/storage/app/uploads/public/5f9/ab0/8c2/5f9ab08c29909806138597.png alt="Screenshot of Greek text with incorrect font">
<em>Correct font used for “μ”:</em> <img src=/storage/app/uploads/public/5f9/ab0/990/5f9ab0990205f215371719.png alt="Screenshot of Greek text with correct font">
<em><code class="language-plaintext highlighter-rouge">Label</code>s with <code class="language-plaintext highlighter-rouge">language</code> property set, using the same <code class="language-plaintext highlighter-rouge">Font</code> instance:</em> <img src=/storage/app/uploads/public/5f9/ab0/a45/5f9ab0a45ba9c930829579.png alt="Screenshot of Arabic scripts">
<em>CJK variants, <code class="language-plaintext highlighter-rouge">Label</code>s using same font instance:</em><img src=/storage/app/uploads/public/5f9/c8b/149/5f9c8b149a866095706571.png alt="Screenshot of CJK variants from a same Font"><h2 id=variable-fonts>Variable fonts</h2><p>Additionally, the new <code class="language-plaintext highlighter-rouge">Font</code> and <code class="language-plaintext highlighter-rouge">TextServer</code>s <abbr title=Bidirectional>BiDi</abbr> and shaping features can work with variable fonts (see <a href=https://github.com/godotengine/godot/pull/43030>godot#43030</a> for the variable font support PR):<p><img src=/storage/app/uploads/public/5f9/ab1/9a7/5f9ab19a7b4d9032991692.gif alt="Variable fonts support"><p>Functions to control the font size were added to the <code class="language-plaintext highlighter-rouge">Theme</code>, <code class="language-plaintext highlighter-rouge">Control</code>, and <code class="language-plaintext highlighter-rouge">Window</code> classes:<ul><li><code class="language-plaintext highlighter-rouge">Control</code> and <code class="language-plaintext highlighter-rouge">Window</code> functions: <code class="language-plaintext highlighter-rouge">add_theme_font_size_override</code>, <code class="language-plaintext highlighter-rouge">get_theme_font_size</code>, <code class="language-plaintext highlighter-rouge">has_theme_font_size</code>, <code class="language-plaintext highlighter-rouge">has_theme_font_size_override</code>.<li><code class="language-plaintext highlighter-rouge">Theme</code> functions: <code class="language-plaintext highlighter-rouge">clear_font_size</code>, <code class="language-plaintext highlighter-rouge">get_font_size</code>, <code class="language-plaintext highlighter-rouge">get_font_size_list</code>, <code class="language-plaintext highlighter-rouge">has_font_size</code>, <code class="language-plaintext highlighter-rouge">set_font_size</code> to control the font size in a similar manner to existing functions for <code class="language-plaintext highlighter-rouge">Font</code>. The special value <code class="language-plaintext highlighter-rouge">-1</code> can be used as unset/default (have the same effect as <code class="language-plaintext highlighter-rouge">null</code> for the font).</ul><h1 id=ui-mirroring>UI mirroring</h1><p>To ensure that the content is easy to understand, user interfaces for the right-to-left written languages should flow from right-to-left. UI “mirroring” provides a convenient way to achieve this without designing separate interfaces for the <abbr title=Right-to-Left>RTL</abbr> and <abbr title=Left-to-Right>LTR</abbr> languages.<p><em>Right-to-left UI:</em> <img src=/storage/app/uploads/public/5f9/ab0/bd9/5f9ab0bd9bc7f519888172.png alt="UI mirroring for RTL written languages">
<em>Left-to-right UI:</em> <img src=/storage/app/uploads/public/5f9/ab0/c09/5f9ab0c095876209674720.png alt="UI mirroring for LTR written languages"><p>In most cases, UI mirroring should happen automatically, and do not require any actions from the user or knowledge of the RTL writing systems.<p>Each <code class="language-plaintext highlighter-rouge">Control</code> has a <code class="language-plaintext highlighter-rouge">layout_direction</code> property to control mirroring. It can be set to <code class="language-plaintext highlighter-rouge">inherited</code> (use the same direction as the parent control, same as <code class="language-plaintext highlighter-rouge">auto</code> for the root control), <code class="language-plaintext highlighter-rouge">auto</code> (direction is determined based on the current locale), or forced to RTL or LTR.<p>On the above screenshots, the green and blue “compass” controls are forced to LTR and RTL layout respectively, the red container is set to <code class="language-plaintext highlighter-rouge">auto</code> and the rest of the UI uses <code class="language-plaintext highlighter-rouge">inherited</code> (default setting) direction.<p>For RTL languages, Godot will automatically do the following changes to the UI:<ul><li>Mirror left/right anchors and margins.<li>Swap left and right text alignment.<li>Mirror horizontal order of the child controls in the containers, and items in <code class="language-plaintext highlighter-rouge">Tree</code>/<code class="language-plaintext highlighter-rouge">ItemList</code> controls.<li>Use mirrored order of the internal control elements (e.g. <code class="language-plaintext highlighter-rouge">OptionButton</code> dropdown button, checkbox alignment, <code class="language-plaintext highlighter-rouge">List</code> column order, <code class="language-plaintext highlighter-rouge">Tree</code> item icons and connecting line alignment, etc.), in some cases mirrored controls use separate theme styles.<li>The coordinate system is not mirrored, and non-UI nodes (sprites, etc.) are not affected.</ul><h1 id=bidi-override-for-structured-text>BiDi override for structured text</h1><p>The Unicode <abbr title=Bidirectional>BiDi</abbr> algorithm is designed to work with natural text and its incapable of handling text with a higher level order, like file names, URIs, email addresses, regular expressions or source code.<p>The <code class="language-plaintext highlighter-rouge">structured_text_bidi_override</code> property and the <code class="language-plaintext highlighter-rouge">_structured_text_parser</code> callback are added to the all text controls to handle this.<p><em>File path display:</em> <img src=/storage/app/uploads/public/5f9/ab0/cd0/5f9ab0cd03075487056412.png alt="File paths with BiDi override"><p>For example, the path for the directory structure in the above screenshot will be displayed incorrectly (top <code class="language-plaintext highlighter-rouge">LineEdit</code> control). The <code class="language-plaintext highlighter-rouge">File</code> type structured text override splits text into segments, then the BiDi algorithm is applied to each of them individually to correctly display directory names in any language and preserve correct order of the folders (bottom <code class="language-plaintext highlighter-rouge">LineEdit</code> control).<p>Custom callbacks provide a way to override BiDi for the other types of structured text. For example, the following code splits a text using <code class="language-plaintext highlighter-rouge">:</code> as separator, applies BiDi to each part, and displays them in reversed order. The BiDi override can be used with any control, including input fields (<code class="language-plaintext highlighter-rouge">LineEdit</code>, <code class="language-plaintext highlighter-rouge">TextEdit</code>).<div class="language-gdscript highlighter-rouge"><div class=highlight><pre class=highlight><code><span class=k>func</span> <span class=nf>_structured_text_parser</span><span class=p>(</span><span class=n>args</span><span class=p>,</span> <span class=n>text</span><span class=p>):</span>
<span class=err> </span> <span class=err> </span> <span class=k>var</span> <span class=n>ranges</span> <span class=o>=</span> <span class=p>[]</span>
<span class=err> </span> <span class=err> </span> <span class=k>var</span> <span class=n>offset</span> <span class=o>=</span> <span class=mi>0</span>
<span class=err> </span> <span class=err> </span> <span class=k>for</span> <span class=n>t</span> <span class=ow>in</span> <span class=n>text</span><span class=o>.</span><span class=n>split</span><span class=p>(</span><span class=s2>":"</span><span class=p>):</span>
<span class=err> </span> <span class=err> </span> <span class=err> </span> <span class=err> </span> <span class=k>var</span> <span class=n>text_offset</span> <span class=o>=</span> <span class=n>offset</span> <span class=o>+</span> <span class=n>t</span><span class=o>.</span><span class=n>length</span><span class=p>()</span>
<span class=err> </span> <span class=err> </span> <span class=err> </span> <span class=err> </span> <span class=n>ranges</span><span class=o>.</span><span class=n>push_front</span><span class=p>(</span><span class=n>Vector2i</span><span class=p>(</span><span class=n>offset</span><span class=p>,</span> <span class=n>text_offset</span><span class=p>)</span> <span class=c1># Add text</span>
<span class=err> </span> <span class=err> </span> <span class=err> </span> <span class=err> </span> <span class=n>ranges</span><span class=o>.</span><span class=n>push_front</span><span class=p>(</span><span class=n>Vector2i</span><span class=p>(</span><span class=n>text_offset</span><span class=p>,</span> <span class=n>text_offset</span> <span class=o>+</span> <span class=mi>1</span><span class=p>))</span> <span class=c1># Add ":"</span>
<span class=err> </span> <span class=err> </span> <span class=err> </span> <span class=err> </span> <span class=n>offset</span> <span class=o>=</span> <span class=n>text_offset</span> <span class=o>+</span> <span class=mi>1</span>
<span class=err> </span> <span class=err> </span> <span class=k>return</span> <span class=n>ranges</span>
</code></pre></div></div><h1 id=other-changes-to-the-godot-controls>Other changes to the Godot controls</h1><p>To control BiDi and font related features, all controls have the following properties added:<ul><li><code class="language-plaintext highlighter-rouge">language</code> property (only for controls with text):
Overrides the locale for that node: controls language specific line breaking rules, OpenType localized form, shaping, and font substitution.</ul><p><em>English and Romanian rendering of the same string:</em> <img src=/storage/app/uploads/public/5f9/ab0/d67/5f9ab0d67b297148367651.png alt="English and Romanian rendering of the same string"><ul><li><code class="language-plaintext highlighter-rouge">opentype_features</code> property (only for controls with text):
Controls OpenType font features for the node e.g. ligature types to use, number styles, small caps, etc. (<a href=https://docs.microsoft.com/en-us/typography/opentype/spec/featuretags>Full list of standard tags</a>, fonts can have custom features as well).</ul><p><em>OpenType features:</em> <img src=/storage/app/uploads/public/5f9/ab0/e30/5f9ab0e30a6ab831089813.png alt="Labels using different OpenType features"><h1 id=reference-work>Reference work</h1><ul><li>UI improvements for implementing BiDi aware apps proposal: <a href=https://github.com/godotengine/godot-proposals/issues/1183>godot-proposals#1183</a>.<li>UI mirroring, Font, Theme, and control changes: <a href=https://github.com/godotengine/godot/pull/41100>godot#41100</a> (commits 5 to 8, the PR is regularly rebased to keep up with upstream changes, exact commit hashes may vary).<li>Variable font support PR: <a href=https://github.com/godotengine/godot/pull/43030>godot#43030</a>.<li>Mirroring, BiDi layout and override demo projects: <a href=https://github.com/godotengine/godot-demo-projects/pull/538>godot-demo-projects#538</a>.</ul><h1 id=additional-resources>Additional resources</h1><ul><li><a href=https://docs.microsoft.com/en-us/windows/uwp/design/globalizing/design-for-bidi-text>Windows guidelines to design apps for bidirectional text</a>.<li><a href=https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/RTL_Guidelines>Mozilla RTL guidelines</a>.</ul><h1 id=future>Future</h1><p>The next part will focus on the changes to the specific controls and <code class="language-plaintext highlighter-rouge">RichTextLabel</code> BBCode.</div></div></article></div><link rel=stylesheet href=/assets/css/anchor-link.css?1><link rel=stylesheet href=/assets/css/article-cards.css?2><script src=/assets/js/anchor-link.js></script>
<script>document.addEventListener('DOMContentLoaded',()=>{window.applyAnchorLinks('.article-body'),document.querySelectorAll('.article-cover img, .article-body img').forEach(b=>{if(b.classList.contains('lightbox-ignore'))return;const a=document.createElement('a');a.href=b.src,a.classList.add('lightbox'),a.dataset.group='article',b.parentNode.appendChild(a),a.appendChild(b)})})</script></main><footer><div class="container flex footer-container"><div id=copyright><p>© 2007-2024 Juan Linietsky, Ariel Manzur and <a href=https://github.com/godotengine/godot/blob/master/AUTHORS.md target=_blank rel=noopener>contributors</a>.<br>Hosted by the <a href=https://godot.foundation/ target=_blank rel=noopener>Godot Foundation</a>.<br>Website <a href=https://github.com/godotengine/godot-website target=_blank rel=noopener>source code on GitHub</a>.</div><div id=sitemap><ul class=sitemap-group><li><strong>Get Godot</strong><li><a href=/download/windows/ class=set-os-download-url>Download</a><li><a href=/download/archive/>Release Archive</a><li><a href=https://editor.godotengine.org/releases/latest/>Web Editor</a><li>&nbsp;<li><strong>Public Relations</strong><li><a href=/blog/>Blog</a><li><a href=/community/>Communities and Events</a><li><a href=/press/>Press Kit</a></ul><ul class=sitemap-group><li><strong>About Godot</strong><li><a href=/features/>Features</a><li><a href=/showcase/>Showcase</a><li><a href=/education/>Education</a><li><a href=/license/>License</a><li><a href=/code-of-conduct/>Code of Conduct</a><li><a href=/privacy-policy/>Privacy Policy</a><li><a href=https://fund.godotengine.org>Donate</a></ul><ul class=sitemap-group><li><strong>Project Team</strong><li><a href=/governance/>Governance</a><li><a href=/teams/>Teams</a><li>&nbsp;<li><strong>Extra Resources</strong><li><a href=https://godotengine.org/asset-library/asset>Asset Library</a><li><a href=https://docs.godotengine.org>Documentation</a><li><a href=https://github.com/godotengine>Code Repository</a></ul></div><div id=social class=dark-desaturate><h4 class=text-right><a href=/contact/>Contact us</a></h4><div class="flex justify-space-between" style=gap:3px><a href=https://github.com/godotengine target=_blank rel=noopener><img src=/assets/footer/github_logo.svg width=32 height=32 alt=GitHub></a>
<a href=https://mastodon.gamedev.place/@godotengine target=_blank rel=noopener><img src=/assets/footer/mastodon_logo.svg width=32 height=32 alt=Mastodon></a>
<a href=https://twitter.com/godotengine target=_blank rel=noopener><img src=/assets/footer/twitter_logo.svg width=32 height=32 alt=Twitter></a>
<a href=https://www.facebook.com/groups/godotengine/ target=_blank rel=noopener><img src=/assets/footer/facebook_logo.svg width=32 height=32 alt=Facebook></a>
<a href=https://www.reddit.com/r/godot target=_blank rel=noopener><img src=/assets/footer/reddit_logo.svg width=32 height=32 alt=Reddit></a>
<a href=/rss.xml target=_blank rel=noopener><img src=/assets/footer/feed_logo.svg width=32 height=32 alt="RSS feed"></a></div></div></div></footer><script defer src=/assets/js/tobii.min.js></script>
<script defer src=/assets/js/highlight.min.js?1></script>
<script defer src=/assets/js/highlight.gdscript.min.js?1></script>
<script>document.addEventListener('DOMContentLoaded',()=>{document.querySelectorAll('pre code').forEach(a=>{hljs.highlightBlock(a)}),document.querySelectorAll('[data-post-date]').forEach(a=>{Date.parse(a.dataset.postDate)>Date.now()-1e3*60*60*48&&a.classList.add("post-recent-highlight")}),new Tobii({zoom:!1});const a=document.querySelectorAll('.set-os-download-url');for(let b=0;b<a.length;b++){const c=a[b];let e='download';'version'in c.dataset&&c.dataset.version==='3'&&(e='download/3.x');let d='windows';navigator.platform.indexOf('Mac')!==-1?d='macos':navigator.platform.indexOf('Linux')!==-1&&(d='linux'),c.href=`/${e}/${d}/`}})</script>