-
-
Notifications
You must be signed in to change notification settings - Fork 97
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 layout/shaping API structure #4
Comments
My comments about this are:
|
It depends on what's considered big. HarfBuzz is: 2.5 MB of sources, optimized static lib is 4 MB ICU data 5 to 30 MB (it's only used for dictionary based line breaking and loaded from file or request).
I guess that's reasonable, while it's possible to do some sort of shaping results are far from perfect. |
To summarize I have drafted following minimal API changes:
|
A bit of additional size info, here are symbol sizes in the debug Linux binary with gdtl module measured using Bloaty tool. ICU (≈512.4 KiB)
HarfBuzz (≈531.9 KiB)
Graphite (≈100.6 KiB)
Rest of the modeule code (≈633.5 KiB)
|
Since this stuff is quite heavy, OSs like Windows, macOS and Android have their own shaping engines, shaping engines in general depends on font implementation internals and some users desire to use system fonts, it might be better to move both font handling and shaper to the single module/plugin. Also, using system shapers and font libraries will give UI more native look and feel. Keep BitmapFont and dummy shaper (LTR only, pass thought - maps chars/graphemes one-to-one) as fallback, have FreeType and ICU/HarfBuzz (or whatever will be used) as optional (and default for the editor) module, and allow custom implementations (e.g. based on DirectWrite/CoreText shapers) via GDNative plugins. A quick mock-up of the backend interface that should cover all use cases.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_DIRECTION_RTL // Right-to-left.
};
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).
};
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.
Vector2 offset; // Offset from the origin of the glyph.
};
Vector<Glyph> glyphs;
Vecotor2i range; // Range in the original string this grapheme corresponds to.
float advance = 0.f; // Horizontal advance to the next grapheme.
bool rtl = false; // Direction of the grapheme, can be used to display cursor on the correct side of it.
RID font;
};
struct TextFormat {
Vector2i range; // Range in the original string to apply formatting to.
RID font;
String features; // List of OpenType feature tags, for advanced typography, see https://docs.microsoft.com/en-us/typography/opentype/spec/features_ae
String locale; // ISO language tag.
Variant inline_object; // Inline object id. Can be used to add images/tables etc., for the shaping engine only size of the object and it's position in the string is relevant, usually it's represented as special placeholder character (U+FFFC).
Size2 inline_object_size; // Inline object size.
VAlign inline_object_align; // Inline object offset from the base line.
};
protected:
//......//
public:
virtual void free(RID p_rid) = 0;
// Font API.
virtual RID create_font_from_name(const String &p_name, float p_font_size) = 0; // Loads OS defualt font by name (if supported).
virtual RID create_font_from_res(const String &p_filename, float p_font_size) = 0; // Loads custom font from "res://" "file".
virtual RID create_font(const Vector<uint8_t> &p_data, float p_font_size) = 0; // Loads custom font from memory.
virtual RID font_get_fallback(RID p_font) const = 0;
virtual void font_set_fallback(RID p_font, RID p_fallback) = 0;
virtual float font_get_height(RID p_font) const = 0;
virtual float font_get_ascent(RID p_font) const = 0;
virtual float font_get_descent(RID p_font) const = 0;
virtual float font_get_underline_position(RID p_font) const = 0;
virtual float font_get_underline_thickness(RID p_font) const = 0;
virtual bool font_has_outline(RID p_font) const = 0;
virtual void font_draw_glyph(RID p_canvas, const Vector2 &p_pos, uint32_t p_index, RID p_font, const Color &p_color);
virtual void font_draw_glyph_outline(RID p_canvas, const Vector2 &p_pos, uint32_t p_index, RID p_font, const Color &p_color);
// Shaped text API.
virtual RID shape_plain_text(const String &p_text, RID p_font, const String &p_features, const String &p_locale, TextDirection p_direction) = 0; // Performs BiDi reordering and shaping as a single line.
virtual RID shape_rich_text(const String &p_text, const Vector<TextFormat> &p_formatting, TextDirection p_direction) = 0;
virtual Vector<Grapheme> shaped_get_graphemes(RID p_shaped, const Vector2i &p_range) const = 0; // 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 = 0; // Returns detected base direction of the string if it was shaped with AUTO direction.
virtual Vector2 shaped_get_inline_object_position(RID p_shaped, const Variant &p_inline_object, const Vector2i &p_range) const = 0; // Returns position of the inline object after shaping (position in specific line if range is specified).
virtual Vector<Vector2i> shaped_get_line_breaks(RID p_shaped, float p_width, TextBreak p_break_mode) const = 0; // Returns line ranges, ranges can be directly used with get_graphemes function to render multiline text.
virtual Vector<Vector2i> shaped_get_word_breaks(RID p_shaped, float p_width) const = 0;
virtual Size2 shaped_get_size(RID p_shaped) const = 0;
virtual float shaped_get_ascent(RID p_shaped) const = 0; // 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 = 0; // Also, can include size of inline objects.
virtual float shaped_fit_to_width(RID p_shaped, float p_width, TextJustification p_justification_mode) const = 0; // Adjusts spaces and elongations in the line to fit it to the specified width, returns line width after adjustment.
};
VARIANT_ENUM_CAST(TextServer::TextDirection);
VARIANT_ENUM_CAST(TextServer::TextBreak);
VARIANT_ENUM_CAST(TextServer::TextJustification);
// Text and cursors drawing sample.
RID sh = TS->shape_plain_text(text, font, "", "", AUTO);
Vector<Grapheme> gr = TS->shaped_get_graphemes(sh, Vector2i());
...
Vecotr2 offset;
for (int i = 0; i < gr.size(); i++) {
// Draw glyphs
for (int j = 0; j < gr[i].glpyhs.size(); j++) {
TS->font_draw_glyph(canvas, offset + gr[i].glyphs[j].offset, gr[i].glyphs[j].index, gr[i].font, color);
}
// Draw cursors (In most cases moving cursor only to the grapheme edges is OK. Cursor in the middle of grapheme can be useful, but calculating its position require support from the font which is absent absolute majority of the fonts)
if (gr[i].range.x /*start*/ == cursor_pos) {
if (gr[i].rtl) {
// draw cursor @ offset.x + gr[i].advance
} else {
// draw cursor @ offset.x
}
}
if (gr[i].range.y /*end*/ == cursor_pos) {
if (gr[i].rtl) {
// draw cursor @ offset.x
} else {
// draw cursor @ offset.x + gr[i].advance
}
}
offset.x += gr[i].advance;
}
|
Since CTL proposal was approved and before any GSoC coding begins, lets finalize appropriate CTL APIs structure and breaking changes,
1. Godot
String
API:Current implementation:
wchar_t
type.Proposed implementation:
char16_t
) on all platforms (primary because ICU API is UTF-16 based)2. Text rendering API structure and abstraction level:
Current implementation:
draw_text
,draw_char
) and string size functions (get_string_size
) in bothFont
andCanvasItem
Pros:
Cons:
RichTextLabel
), to archive correct result whole paragraph of the text should be shaped as single entity.Proposed/
libgdtl
implementation:TLShapedString
immutable class to store shaping results, and provide function to perform measurements (string metrics, caret, selection) and shaped text processing (justification, line breaking).TLAttributedShapedString
spannable constructed on top ofTLShapedString
for the Rich text support.TLParagraph
simple wrapper to handle multiple lines of text in the more convenient way (optional, may be part of a multiline control instead of full-blown class)Pros:
Cons:
3. Font fallback:
Current implementation:
BitmapFont
uses reference to the next fallback, in the manner of single-linked list.DynamicFont
uses mainDynamicFontData
and a list of fallbackDynamicFontData
references.Pros:
Cons:
DynamicFontDataForSize
selected during shaping, current implementation doesn't have similar object forBitmapFont
.Proposed/
libgdtl
implementation:TLFontFamily
holding separateTLFontFace
reference lists for scripts/languages (supported scripts can be derived during TTF/OTF font loading, without additional user interaction).Pros:
Cons:
4. Use of ICU library:
In the
libgdtl
provides APIs for BiDi, break-iterators (for justification, line breaking), and UCDN (used for shaping) and encoding conversion.Pros:
Cons:
The text was updated successfully, but these errors were encountered: