This proposal is follow up to #4, to get some community feedback/preferences on a specific parts of CTL support implementation.
Describe the project you are working on:
The Godot Engine
Describe the problem or limitation you are having in your project:
Currently, text display is extremely limited, and only supports simple, left-to-right scripts.
Describe the feature / enhancement and how it helps to overcome the problem or limitation:
Proper display of the text requires multiple steps to be done:
๐น BiDi reordering (placing parts of the text as they are displayed), should be done on the whole paragraph of text, e.g. any part of the text that logically independent of the rest.
Click to expand

๐น Shaping (choosing context dependent glyphs from the font and their relative positions).
Click to expand

๐น Since text in each singe line should maintain logical order, breaking is done on non-reordered text (but it should be shaped, and shaping requires direction to be known, hence text is temporary reordered back for breaking).
Then each line is reordered again (using slightly different algorithm), there's no need for shaping it again, results can be taken from the step 2.
Click to expand

๐น Optionally some advanced technics can be using for line justification, but just expanding spaces should be OK in general.
Click to expand

๐น For some types of data (urls/emails/source code) each part should be processed separately.
Click to expand

๐น Doing these steps is quite expensive, and it's results probably should be cached, and results of the steps 1 and 2 can be reused for steps 3, 4 (e.g. resizing controls).
๐น macOS and Windows have powerful built-in BiDi/shaping engines (CoreText and DirectWrite), and there are open source solutions for both, FreeBidi (LGPL) and ICU (MIT like license) for BiDi (ICU quite big, but also provides tons of potentially useful i18n stuff) and HarfBuzz for shaping (MIT, AFAIK there're no alternatives).
๐น Most shapers only support widely used languages, for more exotic once SIL Graphite (MPL2 or LGPL) can be used (shaping engine for the font is integrated as bytecode into the font itself), which can be used as backend for HarfBuzz.
๐น For the cross-platform engine ICU+Harfbuzz+Graphite seems to be the most logical choice, but we probably should have some way for custom platform specific implementations.
๐น Majority of games do not need any dynamic text (neither dynamic fonts), everything can be pre-rendered as image, probably both (Dynamic font and CTL) should be optional modules to avoid waste of space.
Describe how your proposal will work, with code, pseudocode, mockups, and/or diagrams:
What's the best way to implement it?
Core only, module or GDNative?
๐น Built-in CTL as the only way display text.
๐น Built-in CTL module that can be disabled and the simple fallback module (handling text as it is done now) and GDNative for custom implementations.
๐น Built-in simple fallback and GDNative only CTL (include dynamic library with the editor and export templates).
Module or GDNative providing what?:
๐น Only BiDi/shaping APIs.
๐น Both BiDi/shaping and font implementations. (Simple + BitmapFont/CTL + DynamicFont)
API (Base), How low or high level should it be?
๐น Low level functions (do all the steps in the Font->draw and complex controls):
struct Run {
Start, End;
Script, Direction
};
Vector<Run> bidi_reorder(String, Direction)
Vector<Run> bidi_reorder_substr(String, Start, End, Direction) // Line reordering is different form full reordering and require full paragraph string as input.
Vector<Grapheme> shape_run(String, Run, Font, Language)
Vector<Pos + Type> get_safe_break_points(String)
Vector<Pos + Type> get_justification_points(String)
Or have abstraction to expose all internal implementation structures (e.g. BiDi context, and shaping buffers)
๐น Do BiDi and shaping in the one step, but expose results (see https://github.com/godotengine/godot-proposals/issues/4#issuecomment-636872816)
Vector<Grapheme> shape_string(String, Font, Language, Direction)
Vector<Grapheme> shape_rich_string("Some rich string representation", Direction)
Vector<Grapheme> justify_line(String, Vector<Grapheme>, Width)
Vector<Vector<Grapheme>> break_line(String, /*full line*/ Vector<Grapheme>, Width) // Can reuse shaped grapheme and do BiDi steps only.
or
Vector<Vector<Grapheme>> shape_lines(String, Font, Language, Direction, Width) // Do full reshaping, it's not efficient way to do it, if line width for the same text is changing muliple times, but OK if it done once (or can ue internal cache, to reuse full line data).
๐น High level, use ShapedString structure containing both input and output data as the single entity, w/o need of directly accessing underling low-level stuff, with lazy BiDi/shaping and caching. (see https://github.com/godotengine/godot-proposals/issues/4#issuecomment-490202448)
ShapedAttributedString("Some rich string representation", Direction)
ShapedString->set_string/font/language/direction
Vector<ShapedString> ShapedString->break_lines(Width) // Can reuse preserved paragraph BiDi context, and shaped full line graphemes without extra actions.
void shstr->justify(Width)
๐น High level, use ShapedParagraph structure handling all layout features for a whole paragraphs at once.
๐น Use both ShapedString and Paragraph as higher level helper for multiline controls. (see https://github.com/bruvzg/godot_tl)
๐น Which part of the API should handle caching? Controls and Font or module functions?
๐น Something else?
API (Text input, cursor/selection control), should be handled by controls or module?
๐น Only complex, font specific functions (e.g. ligature cursors), do everything else in the controls.
๐น Common cursor control API for all controls (e.g. ShapedString->move_caret(CursorPos, +/- Magnitude, Type WORD/CHAR/LINE/PARA) -> CursorPos, ShapedStgring->hit_test(..., Coords) -> CursorPos).
๐น Something else?
API (Font, Canvas)
Currently, we have duplicate string drawing functions both in Canvas and Font, do we need both?
If this enhancement will not be used often, can it be worked around with a few lines of script?:
It will be used to draw all text in the editor and exported apps.
Is there a reason why this should be core and not an add-on in the asset library?:
Main implementation can and probably should be module, and have support for the custom GDNative implementations, but substantial changes to the core are required anyway.
Current list of proposed changes:
CharString (Char32String ?), for file and macOS/Linux wchar_t access.Char32String utf32() const;bool parse_utf32(const char32_t *p_utf8, int p_len = -1);static String utf32(const char32_t *p_utf8, int p_len = -1);wchar_t macros for wchar_t APIs (used almost exclusively on Windows, where it will return ptr directly)#ifdef WINDOWS_ENABLED
#define FROM_WC_STR(m_value, m_len) (String((const CharType *)(m_value), (m_len)))
#define WC_STR(m_value) ((const wchar_t *)((m_value).get_data()))
#else
#define FROM_WC_STR(m_value, m_len) (String::utf32((const CharType32 *)(m_value), (m_len)))
#define WC_STR(m_value) ((const wchar_t *)((m_value).utf32().get_data()))
#endif
char32_t (Char32Type ?) and wchar_t constructorsc_str to get_data?static bool is_single(char16_t p_char);
static bool is_surrogate(char16_t p_char);
static bool is_surrogate_lead(char16_t p_char);
static bool is_surrogate_trail(char16_t p_char);
static char32_t get_supplementary(char16_t p_lead, char16_t p_trail);
static char16_t get_lead(char32_t p_supplementary);
static char16_t get_trail(char32_t p_supplementary);
ord to read supplementary charssize and lengthsize - return UTF-16 code point count + termination 0length - return char countselect_word and word_wrap and use TextServer insteaducaps.h and move _find_upper and _find_lower to TextServer (reuse old implementations for fallback TextServer)TextServer for text to digit decoding to handle non 0123456789 numeralswchar_t is only used for few debug prints and assimp file names)Variant to reflect string changesVector<uint8_t> to/from string functions for all UTF-8 / UTF-16 and UTF-32 variants for file accessAPI mock-up for TextServer base class:
class TextServer : public Object {
* GDCLASS(TextServer, Object);
public:
enum TextDirection {
TEXT_DIRECTION_AUTO, // Detects text direction based on string content and specific locale
TEXT_DIRECTION_LTR, // Left-to-right text.
TEXT_DIRECTION_RTL // Right-to-left text.
};
enum TextOrientation {
TEXT_ORIENTATION_HORIZONTAL_TB, // Text flows horizontally, next line to under
TEXT_ORIENTATION_VERTICAL_RL, // For LTR text flows vertically top to bottom, next line is to the left. For RTL, text flows from bottom to top, next line to the right. Vertical scripts displayed upright.
TEXT_ORIENTATION_VERTICAL_LR, // For LTR text flows vertically top to bottom, next line is to the right. For RTL, text flows from bottom to top, next line to the left. Vertical scripts displayed upright.
TEXT_ORIENTATION_SIDEWAYS_RL, // ... Vertical scripts displayed sideways.
TEXT_ORIENTATION_SIDEWAYS_LR
};
enum TextJustification {
TEXT_JUSTIFICATION_NONE = 0,
TEXT_JUSTIFICATION_KASHIDA = 1 << 1, // Change width or add/remove kashidas (ูููู).
TEXT_JUSTIFICATION_WORD_BOUND = 1 << 2, // Adds/removes extra space between the words (for some languages, should add spaces even if there were non in the original string, using dictionary).
TEXT_JUSTIFICATION_GRAPHEME_BOUND = 1 << 3, // Adds/removes extra space in between all non-joining graphemes.
TEXT_JUSTIFICATION_GRAPHEME_WIDTH = 1 << 4 // Adjusts width of the graphemes visually (if supported by font), 10-15% of change should be OK in general.
};
enum TextBreak {
TEXT_BREAK_NONE = 0,
TEXT_BREAK_MANDATORY = 1 << 1, // Breaks line at the explicit line break characters ("\n" etc).
TEXT_BREAK_WORD_BOUND = 1 << 2, // Breaks line between the words.
TEXT_BREAK_GRAPHEME_BOUND = 1 << 3 // Breaks line between any graphemes (in general it's OK to break line anywhere, as long as it isn't reshaped after).
};
enum TextCaretMove {
TEXT_CARET_GRAPHEME,
TEXT_CARET_WORD,
TEXT_CARET_SENTENCE,
TEXT_CARET_PARAGRAPH
};
enum TextGraphemeFlags {
GRAPHEME_FLAG_VALID = 1 << 1,
GRAPHEME_FLAG_RTL = 1 << 2,
GRAPHEME_FLAG_ROTCW = 1 << 3, // For sideways vertical layout.
GRAPHEME_FLAG_ROTCCW = 1 << 4,
};
struct Grapheme {
struct Glyph {
uint32_t glyph_index = 0; // Glyph index is internal value of the font and can't be reused with other fonts, or store UTF-32 codepoint for invalid glyphs (for faster invalid char hex code box display).
Vector2 offset; // Offset from the origin of the glyph.
};
Vector<Glyph> glyphs;
Vector2i range; // Range in the original string this grapheme corresponds to.
Vector2 advance; // Advance to the next glyph.
/*TextGraphemeFlags*/ uint8_t flags; // Used for caret drawing.
RID font;
};
struct Caret {
Rect2 position; // Caret rectangle
bool is_primary;
};
protected:
//......//
virtual bool has_feature(Feature p_ftr); // --> BiDi, Shaping, System Fonts
// Font API
virtual RID create_font_system(const String &p_name); // Loads OS default font by name (if supported).
virtual RID create_font_resource(const String &p_filename); // Loads custom font from "res://" or filesystem.
virtual RID create_font_memory(const Vector<uint8_t> &p_data); // Loads custom font from memory (for built-in fonts).
virtual float font_get_height(RID p_font, float p_size) const;
virtual float font_get_ascent(RID p_font, float p_size) const;
virtual float font_get_descent(RID p_font, float p_size) const;
virtual float font_get_underline_position(RID p_font, float p_size) const;
virtual float font_get_underline_thickness(RID p_font, float p_size) const;
virtual bool font_has_feature(RID p_font, FontFeature p_feature) const; // Outline, Resizable, Distance field
virtual bool font_language_supported(RID p_font, const String &p_locale) const;
virtual bool font_script_supported(RID p_font, const String &p_script) const;
virtual void font_draw_glyph(RID p_font, RID p_canvas, float p_size, const Vector2 &p_pos, uint32_t p_index, const Color &p_color) const;
virtual void font_draw_glyph_outline(RID p_font, RID p_canvas, float p_size, const Vector2 &p_pos, uint32_t p_index, const Color &p_color) const;
virtual void font_draw_invalid_glpyh(RID p_font, RID p_canvas, float p_size, const Vector2 &p_pos, uint32_t p_index, const Color &p_color) const; // Draws box with hex code, scaled to match font size.
// Shaped Text Buffer
virtual RID create_shaped_text(TextDirection p_direction = TEXT_DIRECTION_AUTO, TextOrientation p_orientation = TEXT_ORIENTATION_HORIZONTAL_TB);
virtula void shaped_set_direction(RID p_shaped, TextDirection p_direction = TEXT_DIRECTION_AUTO);
virtula void shaped_set_orientation(RID p_shaped, TextOrientation p_orientation = TEXT_ORIENTATION_HORIZONTAL_TB);
virtual bool shaped_add_text(RID p_shaped, const String &p_text, const List<RID> &p_font, float p_size, const String &p_features = "", const String &p_locale = ""); // Add text and object to span stack, lazy
virutal bool shaped_add_object(RID p_shaped, Variant p_id, const Size2 &p_size, VAlign p_inline_align); // Add inline object
virtual RID shaped_create_substr(RID p_shaped, int p_start, int p_length) const; // Get shaped substring (e.g for line breaking)
virtual Vector<Grapheme> shaped_get_graphemes(RID p_shaped) const; // Returns graphemes as is or BiDi reorders them for the line if range is specified. Graphemes returned in visual (LTR) order. Returned graphems should be usable in the place of characters for the most UI use cases, without massive code changes.
virtual TextDirection shaped_get_direction(RID p_shaped) const; // Returns detected base direction of the string if it was shaped with AUTO direction.
virtual Vector<Vector2i> shaped_get_line_breaks(RID p_shaped, float p_width, /*TextBreak*/ uint8_t p_break_mode) const; // Returns line ranges, ranges can be directly used with get_graphemes function to render multiline text.
virtual Rect2 shaped_get_object_rect(RID p_shaped, Variant p_id) const;
virtual Size2 shaped_get_size(RID p_shaped) const;
virtual float shaped_get_ascent(RID p_shaped) const; // For some languages, graphemes can be offset from the base line significantly, these functions should return maximum ascent and descent, though for most cases using font ascent/descent is OK.
virtual float shaped_get_descent(RID p_shaped) const; // Also, can include size of inline objects.
virtual float shaped_get_line_spacing(RID p_shaped) const; // Offset to the next line (in the direction specified by text orientation)
virtual float shaped_fit_to_width(RID p_shaped, float p_width, /*TextJustification*/ uint8_t p_justification_mode) const; // Adjusts spaces and elongations in the line to fit it to the specified width, returns line width after adjustment.
// Shaped Text Buffer helpers for input controls
virtual Vector<Caret> shaped_get_carets(RID p_shaped, int p_pos) const;
virtual Vector<Rect2> shaped_get_selection(RID p_shaped, int p_start, int p_end) const;
virtual int shaped_hit_test(RID p_shaped, const Vector2 &p_coords) const;
// String API
virtual bool string_get_word(const String &p_string, int p_offset, int &r_beg, int &r_end) const;
virtual bool string_get_line(const String &p_string, int p_offset, int &r_beg, int &r_end) const;
virtual int caret_advance(const String &p_string, int p_value, TextCaretMove p_type) const;
virtual bool is_uppercase(char32_t p_char) const;
virtual bool is_lowercase(char32_t p_char) const;
virtual bool is_titlecase(char32_t p_char) const;
virtual bool is_digit(char32_t p_char) const;
virtual bool is_alphanumeric(char32_t p_char) const;
virtual bool is_punctuation(char32_t p_char) const;
virtual char32_t to_lowercase(char32_t p_char) const;
virtual char32_t to_uppercase(char32_t p_char) const;
virtual char32_t to_titlecase(char32_t p_char) const;
virtual int32_t to_digit(char32_t p_char, int p_radix) const;
// Common
virtual bool load_data(const String &p_filename); // Load custom ICU data file.
virtual void free(RID p_rid);
FontData class -> wrapper for TextServer font API for user/gdscript/gdnative access (in the similar manner wrappers like Texture2D works)class FontData : public Resource {
GDCLASS(FontData, Resource);
protected:
static void _bind_methods();
public:
RID get_rid() const;
bool load_system(const String &p_name);
bool load_resource(const String &p_filename);
bool load_memory(const Vector<uint8_t> &p_data);
float get_height(float p_size) const;
float get_ascent(float p_size) const;
float get_descent(float p_size) const;
float get_underline_position(float p_size) const;
float get_underline_thickness(float p_size) const;
bool has_feature(Feature p_feature) const;
bool language_supported(const String &p_locale) const;
bool script_supported(const String &p_script) const;
void font_draw_glyph(RID p_canvas, float p_size, const Vector2 &p_pos, uint32_t p_index, const Color &p_color) const;
void font_draw_glyph_outline(RID p_canvas, float p_size, const Vector2 &p_pos, uint32_t p_index, const Color &p_color) const;
void font_draw_invalid_glpyh(RID p_canvas, float p_size, const Vector2 &p_pos, uint32_t p_index, const Color &p_color) const;
}
Font class -> generic font funtionsFontData (in the similar manner DynamicFont is currntly doing) + language / script support overridesclass Font : public Resource {
GDCLASS(Font, Resource);
protected:
static void _bind_methods();
public:
float get_height(float p_size) const;
float get_ascent(float p_size) const;
float get_descent(float p_size) const;
float get_underline_position(float p_size) const;
float get_underline_thickness(float p_size) const;
Size2 get_string_size(const String &p_string, float p_size) const;
Size2 get_wordwrap_string_size(const String &p_string, float p_size, float p_width) const;
void draw(RID p_canvas_item, float p_size, const Point2 &p_pos, const String &p_text, const Color &p_modulate = Color(1, 1, 1), int p_clip_w = -1, const Color &p_outline_modulate = Color(1, 1, 1)) const;
void draw_halign(RID p_canvas_item, float p_size, const Point2 &p_pos, const String &p_text, HAlign p_align, float p_width, const Color &p_modulate = Color(1, 1, 1), const Color &p_outline_modulate = Color(1, 1, 1)) const;
void draw_wordwrap(RID p_canvas_item, float p_size, const Point2 &p_pos, const String &p_text, HAlign p_align, float p_width, const Color &p_modulate = Color(1, 1, 1), const Color &p_outline_modulate = Color(1, 1, 1)) const;
void add_data(const Ref<FontData> &p_data);
void set_data(int p_idx, const Ref<FontData> &p_data);
void set_data_language_support_override(int p_idx, const Vector<String> &p_locales);
void set_data_script_support_override(int p_idx, const Vector<String> &p_scripts);
int get_data_count() const;
Ref<FontData> get_data(int p_idx) const;
void remove_data(int p_idx);
List<RID> get_data_for_locale(const String &p_locale, const String &p_script);
get_height / get_ascent / get_descent - returns max value of all FontDataget_string_size / get_wordwrap_string_size / draw / draw_halign / draw_wordwrap - use LRU cached shaped text.draw_char - remove it completely to avoid abuse, draw can be used instead in all use cases.TextServer shaped buffer API for gdscrip/gdnative access (in the similar manner wrappers like Texture2D works).class ShapedText : public Reference {
GDCLASS(ShapedText, Reference);
protected:
void set_direction(TextDirection p_direction);
TextDirection get_direction() const;
void set_orientation(TextOrientation p_orientation);
TextOrientation get_orientation() const;
bool add_text(const String &p_text, const Ref<Font> &p_font, float p_size, const String &p_features = "", const String &p_locale = "");
bool add_object(Variant p_id, const Size2 &p_size, VAlign p_inline_align);
Ref<ShapedText> substr(int p_start, int p_length) const;
Vector<Grapheme> get_graphemes() const;
Vector<Ref<ShapedText>> break_lines(float p_width, /*TextBreak*/ uint8_t p_break_mode) const;
Rect2 get_object_rect(Variant p_id) const;
Size2 get_size() const;
float get_ascent() const;
float get_descent() const;
float get_line_spacing() const;
float fit_to_width(float p_width, /*TextJustification*/ uint8_t p_justification_mode) const;
void draw(RID p_canvas_item, const Point2 &p_pos, const Color &p_modulate = Color(1, 1, 1), const Color &p_outline_modulate = Color(1, 1, 1)) const;
Vector<Caret> get_carets(int p_pos) const;
Vector<Rect2> get_selection(int p_start, int p_end) const;
int hit_test(const Vector2 &p_coords) const;
};
layout_direction property (LTR / RTL / Inherited (parent control direction) / Auto (based on locale)) for UI mirroring, and mirrored layouts for drawing (same option controls base text direction).PopupMenu -> default align, shortcut/submenu arrow/icon position_activate_submenu - submenu popup direction swap_draw()Button -> icon position_notification -> DRAWCheckBox / CheckButton -> box/label swap, label align_notification -> DRAWOptionButton / SpinBox -> arrow button position, default text and dropdown option align_notification -> DRAW_adjust_width_for_icon, LineEdit positionLinkButton -> text align_notification -> DRAWlayout_direction, v-scroll bar positionContainer: fit_child_in_rectHBoxContainer: _resort() -> draw passGridContainer: _notification -> SORT_CHILDRENSplitContainer: _resort() and _notification -> DRAW and _gui_inputScrollContainer: _update_scrollbar_position(), update_scrollbars(), _notification -> SORT_CHILDRENTabContainer: _gui_input, _notification -> RESIZE, DRAW, get_tab_idx_at_pointGraphEdit -> v-scroll and zoom controls position, context probably should not be affectedtop_layer -> zoom_hb and v_scroll anchors/pos (ctr, _notification -> READY, RESIZE)ProgressBar -> fill direction_notification -> DRAWTextureProgress -> add fill START_TO_END, END_TO_START options ?ItemList -> label align, and icon mode order_notification -> DRAW (scroll anchors, rect_cache)TextServer API for multiline icon textLabel / LineEdit -> default align (probably should add Start/End align options to follow layout and keep Left/Right as fixed directions)total_char_cache -> non-space and nl chars?regenerate_word_cache), use TS instead_gui_input: TS based caret movement, and hit testswindow_pos, store both string offset and px offsetTextServer API for multiline and inputTabs -> tab order_update_cache()TextEdit -> default align for text, scroll bar_update_line_cache -> change to use TS instead of custom width/wrap cacheset_line_wrap_amount / clear_width_cache / clear_wrap_cache -> replace with TS based _update_wrap_cache(width)get_char_width_update_selection_mode_word / get_word_under_cursor / _get_column_pos_of_word -> use TS word bounds._notification -> DRAW, move code highlighting to the cache update, and use it to tokenize source code for shaping._get_mouse_pos / _get_cursor_pixel_pos / get_line_wrap_index_at_col / get_char_pos_for_line / get_column_x_offset_for_line / get_char_pos_for / get_column_x_offset -> use TS helpers and TextEdit::Text cache._update_wrap_at / line_wraps / times_line_wraps / get_wrap_rows_text -> remove, do line breaking in TextEdit::Text::_update_wrap_cacheTree -> align and tree line (hflip) / expander positiondraw_item / draw_item_rect - icon pos, END/START alignpropagate_mouse_event column posupdate_scrollbars_notification -> DRAWRichTextLabelRichTextLabel::Line with TS based cache_invalidate_current_line / _validate_line_caches -> separate invalidate_text (text or font) and invalidate_layout(resize, align, indent)_process_line / _find_click / _update_scroll / _notification->DRAW -> rewrite using TS based cache (with full or layout only update options)RichTextEffect - probably should have options to apply it to words instead chars/graphemes, it be more usable with any language.RichTextEdit - maybeCases of Font -> get_ascent / get_descent / get_height usage:
AnimationTrackEdit / AnimationTimelineEdit / AnimationTrackEditGroup / AnimationBezierTrackEdit -> _notificationAnimationTrackEdit / AnimationTrackEditSubAnim / AnimationTrackEditTypeAnimation -> draw_keyEditorProperty / EditorInspectorCategory / EditorInspectorSection / EditorPropertyLayersGrid -> _notification / get_minimum_sizeEditorPropertyEasing / CustomPropertyEditor -> _draw_easingEditorSpinSlider -> _notificationEditorPerformanceProfiler -> _monitor_drawEditorVisualProfiler -> _graph_tex_drawAnimationNodeBlendSpace1DEditor / AnimationNodeBlendSpace2DEditor -> _blend_space_drawAnimationNodeStateMachineEditor -> _state_machine_drawTextureEditor -> _notificationButton / LinkButton / TextEdit / PopupMenu / ProgressBar / TabContainer / Tabs / Label / LineEdit / GraphNode / ItemList -> _notificationTree -> draw_item_rect / draw_itemRichTextLabel -> _process_lineViewport -> _sub_window_updateEditorAudioMeterNotches -> get_minimum_size / _draw_audio_notchesCanvasItemEditor -> _draw_rulersCurveEditor -> update_view_transform / _drawBoneTransformEditor -> _notificationCases of Font -> draw_char / get_char_size usage:
ViewportRotationControl -> _draw_axis (OK, used to draw "XYZ")ItemList -> _notification (not OK, used to draw icon mode text)Label / LineEdit / RichTextLabel / TextureEdit -> _notification (not OK, used to draw all text)AnimationTimelineEdit -> _notification (OK, used to get max digit width)EditorHelp -> _class_desc_resized (OK, used for margins, maybe font should have em_width and en_width functions for placeholder sizes)CanvasItem -> draw_charCases of Font -> draw_string and get_string_size used subsequently (local cache can be used):
AnimationTimelineEdit / EditorInspectorCategory / EditorSpinSlider -> _notificationEditorPerformanceProfiler -> _monitor_drawEditorVisualProfiler -> _graph_tex_drawAbstractPolygon2DEditor -> forward_canvas_draw_over_viewportCanvasItemEditor -> _draw_text_at_position / _draw_guides / _draw_hoverEditorFontPreviewPlugin -> generate_from_pathTextureEditor -> _notificationTileSetEditor -> _on_workspace_overlay_drawButton -> _notification (multiple uses)ItemList -> _notificationLinkButton -> _notificationPopupMenu -> _draw (multiple uses)Tree -> draw_item_rectViewport -> _sub_window_updateget_wordwrap_string_size is not used anywhere.Auto include ICU database to exported project.
EditorExportPlugin for data.There's this asset that I wrote, which adds a new label node with simple BiDi reordering and Arabic shaping. It's written in GDScript, so it's probably not useful here, but perhaps something similar could be made for a fallback module, if any.
Proposal looks great, Love the idea of having a TextServer and optionally making use of platform implementations to avoid having to include ICU or similar in all the export templates.
My only feedback is that I am not sure if its worth having String as UTF16 (as opposed to just using UCS-4 everywhere). Nowadays platforms too much memory to make it worth saving it on this, and strings will never take up that much space.
My only feedback is that I am not sure if its worth having String as UTF16 (as opposed to just using UCS-4 everywhere). Nowadays platforms too much memory to make it worth saving it on this, and strings will never take up that much space.
The only reason for UTF-16 is ICU, which is using it for its APIs.
@bruvzg but most string manipulation in Godot assumes UCS, from parsers to text editors and all other stuff, so I feel it may be a better idea to, worst case, just convert to UTF16 when calling ICU, I am not sure if this has a cost other than converting the string, though.
Converting should be fast, and ICU have its own API for it. I guess we can go with UTF-32 (and only convert it to UTF-16 to get BiDi runs).
If it's gonna cause too much trouble, moving to UTF-16 after the rest of the CTL stuff is implemented won't be any harder than doing it first.
And some ICU APIs already moved to extendable UText abstraction layer which can be used with UTF-32 strings directly (BiDi is currently not one of them, but eventually we'll be able to get rid of convertion).
worst case, just convert to UTF16 when calling ICU, I am not sure if this has a cost other than converting the string, though.
Done some testing, conversion cost is quite low, going with UTF-32 should be fine.
Tests done with the Noto font sample texts (about 11 % of the strings have characters outside BMP).
sounds great then!
Where would I plug in (multichannel) signed distance field font atlases for normal and complex layouts?
Edited:
https://github.com/fire/godot/tree/msdf-oct-2020 and the shader at https://github.com/V-Sekai/godot-msdf-project
Edited:
I don't know which proposal describes this feature. I saw a paragraph on it.
Edited:
bruvzg[m]> Currently, there's
distance_field_hinta flag in the bitmap fonts that is passed toRenderingServerCanvas::canvas_item_set_distance_field_mode(this part is not changed in the TextServer PR), but I do not think it's doing anything at all. I guess adding support should only affectdraw_glyphof the bitmap font implementation.
bruvzg[m]> Font itself is setting the aforementioned flag and calling
RenderingServer::canvas_item_add_texture_rect_regionto draw each glyph, SDF should be implemented in theRenderingServer. Seems like there was some implementation in the GLES3, but there's none in the Vulkan renderer.
Most helpful comment
Current list of proposed changes:
Core Changes (String)
CharString(Char32String?), for file and macOS/Linuxwchar_taccess.Char32String utf32() const;bool parse_utf32(const char32_t *p_utf8, int p_len = -1);static String utf32(const char32_t *p_utf8, int p_len = -1);wchar_tmacros forwchar_tAPIs (used almost exclusively on Windows, where it will return ptr directly)char32_t(Char32Type?) andwchar_tconstructorsc_strtoget_data?ordto read supplementary charssizeandlengthsize- return UTF-16 code point count + termination 0length- return char countselect_wordandword_wrapand useTextServerinsteaducaps.hand move_find_upperand_find_lowertoTextServer(reuse old implementations for fallbackTextServer)TextServerfor text to digit decoding to handle non 0123456789 numeralswchar_tis only used for few debug prints and assimp file names)Variantto reflect string changesVector<uint8_t>to/from string functions for all UTF-8 / UTF-16 and UTF-32 variants for file accessTextServer (handles font and text shaper implementations)
API mock-up for TextServer base class:
Core changes (Font)
FontDataclass -> wrapper forTextServerfont API for user/gdscript/gdnative access (in the similar manner wrappers likeTexture2Dworks)Fontclass -> generic font funtionsFontData(in the similar mannerDynamicFontis currntly doing) + language / script support overridesget_height/get_ascent/get_descent- returns max value of all FontDataget_string_size/get_wordwrap_string_size/draw/draw_halign/draw_wordwrap- use LRU cached shaped text.draw_char- remove it completely to avoid abuse,drawcan be used instead in all use cases.Core add (ShapedString/ShapedText)
TextServershaped buffer API for gdscrip/gdnative access (in the similar manner wrappers likeTexture2Dworks).Control changes
layout_directionproperty (LTR / RTL / Inherited (parent control direction) / Auto (based on locale)) for UI mirroring, and mirrored layouts for drawing (same option controls base text direction).PopupMenu-> default align, shortcut/submenu arrow/icon position_activate_submenu- submenu popup direction swap_draw()Button-> icon position_notification->DRAWCheckBox/CheckButton-> box/label swap, label align_notification->DRAWOptionButton/SpinBox-> arrow button position, default text and dropdown option align_notification->DRAW_adjust_width_for_icon, LineEdit positionLinkButton-> text align_notification->DRAWlayout_direction, v-scroll bar positionContainer:fit_child_in_rectHBoxContainer:_resort()-> draw passGridContainer:_notification->SORT_CHILDRENSplitContainer:_resort()and_notification->DRAWand_gui_inputScrollContainer:_update_scrollbar_position(),update_scrollbars(),_notification->SORT_CHILDRENTabContainer:_gui_input,_notification->RESIZE, DRAW,get_tab_idx_at_pointGraphEdit-> v-scroll and zoom controls position, context probably should not be affectedtop_layer->zoom_hbandv_scrollanchors/pos (ctr,_notification->READY, RESIZE)ProgressBar-> fill direction_notification->DRAWTextureProgress-> add fill START_TO_END, END_TO_START options ?ItemList-> label align, and icon mode order_notification->DRAW(scroll anchors,rect_cache)TextServerAPI for multiline icon textLabel/LineEdit-> default align (probably should add Start/End align options to follow layout and keep Left/Right as fixed directions)total_char_cache-> non-space and nl chars?regenerate_word_cache), use TS instead_gui_input: TS based caret movement, and hit testswindow_pos, store both string offset and px offsetTextServerAPI for multiline and inputTabs-> tab order_update_cache()TextEdit-> default align for text, scroll bar_update_line_cache-> change to use TS instead of custom width/wrap cacheset_line_wrap_amount/clear_width_cache/clear_wrap_cache-> replace with TS based_update_wrap_cache(width)get_char_width_update_selection_mode_word/get_word_under_cursor/_get_column_pos_of_word-> use TS word bounds._notification->DRAW, move code highlighting to the cache update, and use it to tokenize source code for shaping._get_mouse_pos/_get_cursor_pixel_pos/get_line_wrap_index_at_col/get_char_pos_for_line/get_column_x_offset_for_line/get_char_pos_for/get_column_x_offset-> use TS helpers andTextEdit::Textcache._update_wrap_at/line_wraps/times_line_wraps/get_wrap_rows_text-> remove, do line breaking inTextEdit::Text::_update_wrap_cacheTree-> align and tree line (hflip) / expander positiondraw_item/draw_item_rect- icon pos, END/START alignpropagate_mouse_eventcolumn posupdate_scrollbars_notification->DRAWRichTextLabelRichTextLabel::Linewith TS based cache_invalidate_current_line/_validate_line_caches-> separate invalidate_text (text or font) and invalidate_layout(resize, align, indent)_process_line/_find_click/_update_scroll/_notification->DRAW-> rewrite using TS based cache (with full or layout only update options)RichTextEffect- probably should have options to apply it to words instead chars/graphemes, it be more usable with any language.RichTextEdit- maybeCases of
Font->get_ascent/get_descent/get_heightusage:AnimationTrackEdit/AnimationTimelineEdit/AnimationTrackEditGroup/AnimationBezierTrackEdit->_notificationAnimationTrackEdit/AnimationTrackEditSubAnim/AnimationTrackEditTypeAnimation->draw_keyEditorProperty/EditorInspectorCategory/EditorInspectorSection/EditorPropertyLayersGrid->_notification/get_minimum_sizeEditorPropertyEasing/CustomPropertyEditor->_draw_easingEditorSpinSlider->_notificationEditorPerformanceProfiler->_monitor_drawEditorVisualProfiler->_graph_tex_drawAnimationNodeBlendSpace1DEditor/AnimationNodeBlendSpace2DEditor->_blend_space_drawAnimationNodeStateMachineEditor->_state_machine_drawTextureEditor->_notificationButton/LinkButton/TextEdit/PopupMenu/ProgressBar/TabContainer/Tabs/Label/LineEdit/GraphNode/ItemList->_notificationTree->draw_item_rect/draw_itemRichTextLabel->_process_lineViewport->_sub_window_updateEditorAudioMeterNotches->get_minimum_size/_draw_audio_notchesCanvasItemEditor->_draw_rulersCurveEditor->update_view_transform/_drawBoneTransformEditor->_notificationCases of
Font->draw_char/get_char_sizeusage:ViewportRotationControl->_draw_axis(OK, used to draw "XYZ")ItemList->_notification(not OK, used to draw icon mode text)Label/LineEdit/RichTextLabel/TextureEdit->_notification(not OK, used to draw all text)AnimationTimelineEdit->_notification(OK, used to get max digit width)EditorHelp->_class_desc_resized(OK, used for margins, maybe font should haveem_widthanden_widthfunctions for placeholder sizes)CanvasItem->draw_charCases of
Font->draw_stringandget_string_sizeused subsequently (local cache can be used):AnimationTimelineEdit/EditorInspectorCategory/EditorSpinSlider->_notificationEditorPerformanceProfiler->_monitor_drawEditorVisualProfiler->_graph_tex_drawAbstractPolygon2DEditor->forward_canvas_draw_over_viewportCanvasItemEditor->_draw_text_at_position/_draw_guides/_draw_hoverEditorFontPreviewPlugin->generate_from_pathTextureEditor->_notificationTileSetEditor->_on_workspace_overlay_drawButton->_notification(multiple uses)ItemList->_notificationLinkButton->_notificationPopupMenu->_draw(multiple uses)Tree->draw_item_rectViewport->_sub_window_updateget_wordwrap_string_sizeis not used anywhere.Export
Auto include ICU database to exported project.
EditorExportPluginfor data.