Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Complex Text Layouts: 1/4] BiDi/Shaping engine API structure, text processing refactoring. #1180

Closed
bruvzg opened this issue Jul 10, 2020 · 11 comments

Comments

@bruvzg
Copy link
Member

bruvzg commented Jul 10, 2020

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

1_bidi

🔹 Shaping (choosing context dependent glyphs from the font and their relative positions).

Click to expand

2_shape

🔹 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

3_line

🔹 Optionally some advanced technics can be using for line justification, but just expanding spaces should be OK in general.

Click to expand

4_just

🔹 For some types of data (urls/emails/source code) each part should be processed separately.

Click to expand

5_except

🔹 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 #4 (comment))

		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 #4 (comment))

		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.

@bruvzg
Copy link
Member Author

bruvzg commented Jul 13, 2020

Current list of proposed changes:


Core Changes (String)

  1. Change String to use UTF-16 internally.
  2. Add UTF-32 string class, similar to CharString (Char32String ?), for file and macOS/Linux wchar_t access.
  3. String:
  • add UTF-32 functions:
    • 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);
  • add 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
  • add char32_t (Char32Type ?) and wchar_t constructors
    • rename c_str to get_data?
    • add UTF-16 support functions (or macros):
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);
  • change ord to read supplementary chars
    • change size and length
      • size - return UTF-16 code point count + termination 0
      • length - return char count
    • remove select_word and word_wrap and use TextServer instead
    • remove ucaps.h and move _find_upper and _find_lower to TextServer (reuse old implementations for fallback TextServer)
    • use TextServer for text to digit decoding to handle non 0123456789 numerals
  1. Changes in the code that is using String
  • change Windows APIs to use WC cast macros (plain C-cast on Windows, on other OSs wchar_t is only used for few debug prints and assimp file names)
  • change Variant to reflect string changes
  • add Vector<uint8_t> to/from string functions for all UTF-8 / UTF-16 and UTF-32 variants for file access
  • update PCRE config to use UTF-16
  • nothing else should be affected
  1. Update GDNative API to reflect string changes
  2. Update and extend string tests to reflect changes.

TextServer (handles font and text shaper implementations)

  1. Fallback, Full(ICU/HB/Graphite) and GDNative wrapper modules

API 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);

Core changes (Font)

  1. Move implementation to the TextServer
  2. Common 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;
}
  1. Font class -> generic font funtions
  • holds list of the FontData (in the similar manner DynamicFont is currntly doing) + language / script support overrides
class 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);
  • returns list copy sorted by language and script, for the use by TextServer.
	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 FontData
  • get_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.

Core add (ShapedString/ShapedText)

  1. Wrapper over 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;
};

Control changes

  1. Base Control
  • add 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 -> DRAW
    • CheckBox / CheckButton -> box/label swap, label align
      • _notification -> DRAW
    • OptionButton / SpinBox -> arrow button position, default text and dropdown option align
      • _notification -> DRAW
      • _adjust_width_for_icon, LineEdit position
    • LinkButton -> text align
      • _notification -> DRAW
    • Containers -> should change child display order based on layout_direction, v-scroll bar position
      • Container: fit_child_in_rect
      • HBoxContainer: _resort() -> draw pass
      • GridContainer: _notification -> SORT_CHILDREN
      • SplitContainer: _resort() and _notification -> DRAW and _gui_input
      • ScrollContainer: _update_scrollbar_position(), update_scrollbars(), _notification -> SORT_CHILDREN
      • TabContainer: _gui_input, _notification -> RESIZE, DRAW, get_tab_idx_at_point
    • GraphEdit -> v-scroll and zoom controls position, context probably should not be affected
      • top_layer -> zoom_hb and v_scroll anchors/pos (ctr, _notification -> READY, RESIZE)
    • ProgressBar -> fill direction
      • _notification -> DRAW
    • TextureProgress -> add fill START_TO_END, END_TO_START options ?
    • ItemList -> label align, and icon mode order
      • _notification -> DRAW (scroll anchors, rect_cache)
      • directly use TextServer API for multiline icon text
    • Label / 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?
      • remove custom line breaking and word cache (regenerate_word_cache), use TS instead
      • text rendering changes are straightforward
      • need special handling mode for path/URLs/emails -> some user customizable handler for other data types (callback taking text and returning text ranges in the order they should be displayed)
      • _gui_input: TS based caret movement, and hit tests
      • window_pos, store both string offset and px offset
      • directly use TextServer API for multiline and input
      • LineEdit -> add text direction override property
      • LineEdit -> popup menu - add base direction change and BiDi control char insert menus
    • Tabs -> tab order
      • _update_cache()
    • TextEdit -> default align for text, scroll bar
      • line number / breakpoints and tab direction probably should not be mirrored (at least for code editor).
      • replace custom wrap/line/width caches with ShapedText
        • _update_line_cache -> change to use TS instead of custom width/wrap cache
        • set_line_wrap_amount / clear_width_cache / clear_wrap_cache -> replace with TS based _update_wrap_cache(width)
        • remove get_char_width
        • _update_selection_mode_word / get_word_under_cursor / _get_column_pos_of_word -> use TS word bounds.
        • text edit will need special handling mode for code (probably can be mixed with highlighting)
          • _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_cache
    • Tree -> align and tree line (hflip) / expander position
      • draw_item / draw_item_rect - icon pos, END/START align
      • propagate_mouse_event column pos
      • update_scrollbars
      • _notification -> DRAW
    • RichTextLabel
      • BBCode -> item stack is OK
        • keep and compare old item stack to presreve unchanged parts.
      • replace RichTextLabel::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)
        • use fully separete code for cahce update / hit tests and drawing
        • item_text/image/font -> add data to the current frame cache
        • item_tabel/list -> process recursivly
        • item indent/align -> set current frame chace options
        • item_frame/new_line or end of current frame -> shape/linebreak and align current frame cache
        • item_color/underline/fx -> ignore during processing, apply on draw only
          • RichTextEffect - probably should have options to apply it to words instead chars/graphemes, it be more usable with any language.
      • If it's too slow, for the large text, do processing in the bg thread, and display blurry placeholder (with estimated size), process lines on screen first.
    • RichTextEdit - maybe
  • some editor parts should have own sub container with layout always set to LTR (probably there are more)
    • Dock position popup

Cases of Font -> get_ascent / get_descent / get_height usage:

  • AnimationTrackEdit / AnimationTimelineEdit / AnimationTrackEditGroup / AnimationBezierTrackEdit -> _notification
  • AnimationTrackEdit / AnimationTrackEditSubAnim / AnimationTrackEditTypeAnimation -> draw_key
  • EditorProperty / EditorInspectorCategory / EditorInspectorSection / EditorPropertyLayersGrid -> _notification / get_minimum_size
  • EditorPropertyEasing / CustomPropertyEditor -> _draw_easing
  • EditorSpinSlider -> _notification
  • EditorPerformanceProfiler -> _monitor_draw
  • EditorVisualProfiler -> _graph_tex_draw
  • AnimationNodeBlendSpace1DEditor / AnimationNodeBlendSpace2DEditor -> _blend_space_draw
  • AnimationNodeStateMachineEditor -> _state_machine_draw
  • TextureEditor -> _notification
  • Button / LinkButton / TextEdit / PopupMenu / ProgressBar / TabContainer / Tabs / Label / LineEdit / GraphNode / ItemList -> _notification
  • Tree -> draw_item_rect / draw_item
  • RichTextLabel -> _process_line
  • Viewport -> _sub_window_update
  • EditorAudioMeterNotches -> get_minimum_size / _draw_audio_notches
  • CanvasItemEditor -> _draw_rulers
  • CurveEditor -> update_view_transform / _draw
  • BoneTransformEditor -> _notification

Cases 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_char

Cases of Font -> draw_string and get_string_size used subsequently (local cache can be used):

  • AnimationTimelineEdit / EditorInspectorCategory / EditorSpinSlider -> _notification
  • EditorPerformanceProfiler -> _monitor_draw
  • EditorVisualProfiler -> _graph_tex_draw
  • AbstractPolygon2DEditor -> forward_canvas_draw_over_viewport
  • CanvasItemEditor -> _draw_text_at_position / _draw_guides / _draw_hover
  • EditorFontPreviewPlugin -> generate_from_path
  • TextureEditor -> _notification
  • TileSetEditor -> _on_workspace_overlay_draw
  • Button -> _notification (multiple uses)
  • ItemList -> _notification
  • LinkButton -> _notification
  • PopupMenu -> _draw (multiple uses)
  • Tree -> draw_item_rect
  • Viewport -> _sub_window_update
  • Note: get_wordwrap_string_size is not used anywhere.

Export

Auto include ICU database to exported project.

  • EditorExportPlugin for data.

@3akev
Copy link

3akev commented Jul 19, 2020

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.

@reduz
Copy link
Member

reduz commented Jul 21, 2020

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.

@reduz
Copy link
Member

reduz commented Jul 21, 2020

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.

@bruvzg
Copy link
Member Author

bruvzg commented Jul 21, 2020

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.

@reduz
Copy link
Member

reduz commented Jul 23, 2020

@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.

@bruvzg
Copy link
Member Author

bruvzg commented Jul 23, 2020

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).

@bruvzg
Copy link
Member Author

bruvzg commented Jul 24, 2020

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).

  • UTF-32 to UTF-16 conversion ≈ 0.2 % (includes conversion itself and substring range recalculation back to original string).
  • BiDi reordering ≈ 5.4 %
  • Script detection ≈ 9.0 %
  • Shaping ≈ 85.4 % (with single fallback font, about 23 % of glyphs taken from the fallback).

@reduz
Copy link
Member

reduz commented Jul 26, 2020

sounds great then!

@fire
Copy link
Member

fire commented Oct 23, 2020

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_hint a flag in the bitmap fonts that is passed to RenderingServerCanvas::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 affect draw_glyph of the bitmap font implementation.

bruvzg[m]> Font itself is setting the aforementioned flag and calling RenderingServer::canvas_item_add_texture_rect_region to draw each glyph, SDF should be implemented in the RenderingServer. Seems like there was some implementation in the GLES3, but there's none in the Vulkan renderer.

@bruvzg
Copy link
Member Author

bruvzg commented Dec 14, 2020

Proposal is implemented in godotengine/godot#41100 and godotengine/godot#42595.

@bruvzg bruvzg closed this as completed Dec 14, 2020
ShalokShalom added a commit to ShalokShalom/FiraCode that referenced this issue Jul 11, 2022
Godot resolved the hindering issue 2 years ago. 
Godot 4 ships by default with ligatures.
Issue godotengine/godot-proposals#1180
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants