diff --git a/CHANGELOG.md b/CHANGELOG.md index f0042ce2..fc69c5b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,39 @@ +# 0.93.1 - Aug 11, 2021 + +Added: + +- ShapingOptions + - Groups together FontMgr, FontFeature[], leftToRight + - Adds approximateSpaces and approximatePunctuation +- FontMgrRunIterator(String, Font) +- Shaper::shapeLine(String, Font) + +Changed: + +- AnimationDisposalMethod -> AnimationDisposalMode + +Signature updated from: + + - TextLine::make(String, Font, FontFeature[], boolean) + - FontMgrRunIterator(ManagedString, boolean, Font, FontMgr) + - FontMgrRunIterator(String, Font, FontMgr) + - Shaper::shape(String, Font, boolean, float, Point) + - Shaper::shape(String, Font, FontMgr, FontFeature[], boolean, float, RunHandler) + - Shaper::shape(String, Iterator, Iterator, Iterator, Iterator, FontFeature[], float width, RunHandler) + - Shaper::shape(ManagedString, Iterator, Iterator, Iterator, Iterator, FontFeature[], float width, RunHandler) + - Shaper::shapeLine(String, Font, FontFeature[], boolean) + +to: + + - TextLine::make(String, Font, ShapingOptions) + - FontMgrRunIterator(ManagedString, boolean, Font, ShapingOptions) + - FontMgrRunIterator(String, Font, ShapingOptions) + - Shaper::shape(String, Font, ShapingOptions, float, Point) + - Shaper::shape(String, Font, FontMgr, ShapingOptions, float, RunHandler) + - Shaper::shape(String, Iterator, Iterator, Iterator, Iterator, ShapingOptions, float width, RunHandler) + - Shaper::shape(ManagedString, Iterator, Iterator, Iterator, Iterator, ShapingOptions, float width, RunHandler) + - Shaper::shapeLine(String, Font, ShapingOptions) + # 0.93.0 - Aug 10, 2021 Changed: diff --git a/examples/jwm/script/run.py b/examples/jwm/script/run.py index 6cb22067..168897e4 100755 --- a/examples/jwm/script/run.py +++ b/examples/jwm/script/run.py @@ -8,7 +8,7 @@ def main(): parser = argparse.ArgumentParser() parser.add_argument('--skija-version') - parser.add_argument('--jwm-version', default='0.1.3') + parser.add_argument('--jwm-version', default='0.1.170') parser.add_argument('--jwm-dir', default=None) (args, _) = parser.parse_known_args() @@ -46,7 +46,7 @@ def main(): os.chdir(common.root + '/examples/jwm') sources = common.glob('src', '*.java') + common.glob('../scenes/src', '*.java') - common.javac(sources, 'target/classes', classpath = classpath) + common.javac(sources, 'target/classes', classpath = classpath, release = '15', opts = ['--enable-preview']) # Java common.check_call([ @@ -54,6 +54,7 @@ def main(): '--class-path', common.classpath_separator.join(['target/classes'] + classpath)] + (['-XstartOnFirstThread'] if 'macos' == common.system else []) + ['-Djava.awt.headless=true', + '--enable-preview', '-enableassertions', '-enablesystemassertions', '-Xcheck:jni', diff --git a/examples/jwm/src/Main.java b/examples/jwm/src/Main.java index 53a2be48..5a461dae 100644 --- a/examples/jwm/src/Main.java +++ b/examples/jwm/src/Main.java @@ -23,15 +23,17 @@ public class Main implements Consumer { public Main() { Stats.enabled = true; - _window = App.makeWindow(); - _window.setEventListener(this); - changeLayer(); - var scale = _window.getScale(); - _window.resize((int) (1440 * scale), (int) (810 * scale)); - _window.move((int) (240 * scale), (int) (135 * scale)); - _window.show(); - accept(EventReconfigure.INSTANCE); - _window.requestFrame(); + App.makeWindow((window) -> { + _window = window; + _window.setEventListener(this); + changeLayer(); + var scale = _window.getScale(); + _window.resize((int) (1440 * scale), (int) (810 * scale)); + _window.move((int) (240 * scale), (int) (135 * scale)); + _window.show(); + accept(EventReconfigure.INSTANCE); + _window.requestFrame(); + }); } public void paint() { @@ -78,28 +80,35 @@ public void accept(Event e) { } else if (e instanceof EventMouseMove) { _xpos = (int) (((EventMouseMove) e).getX() / _window.getScale()); _ypos = (int) (((EventMouseMove) e).getY() / _window.getScale()); - } else if (e instanceof EventKeyboard) { - EventKeyboard eventKeyboard = (EventKeyboard) e; + } else if (e instanceof EventKeyboard eventKeyboard) { if (eventKeyboard.isPressed() == true) { - if (eventKeyboard.getKeyCode() == 1) { // s - Scenes.stats = !Scenes.stats; - Stats.enabled = Scenes.stats; - } else if (eventKeyboard.getKeyCode() == 5) { // g - System.out.println("Before GC " + Stats.allocated); - System.gc(); - } else if (eventKeyboard.getKeyCode() == 37) { // l - _layerIdx = (_layerIdx + _layers.length - 1) % _layers.length; - changeLayer(); - } else if (eventKeyboard.getKeyCode() == 123) { // ← - Scenes.prevScene(); - } else if (eventKeyboard.getKeyCode() == 124) { // → - Scenes.nextScene(); - } else if (eventKeyboard.getKeyCode() == 125) { // ↓ - Scenes.currentScene().changeVariant(1); - } else if (eventKeyboard.getKeyCode() == 126) { // ↑ - Scenes.currentScene().changeVariant(-1); - } else - System.out.println("Key pressed: " + eventKeyboard.getKeyCode()); + switch (eventKeyboard.getKey()) { + case S -> { + Scenes.stats = !Scenes.stats; + Stats.enabled = Scenes.stats; + } + case G -> { + System.out.println("Before GC " + Stats.allocated); + System.gc(); + } + case L -> { + _layerIdx = (_layerIdx + _layers.length - 1) % _layers.length; + changeLayer(); + } + case LEFT -> + Scenes.prevScene(); + + case RIGHT -> + Scenes.nextScene(); + + case DOWN -> + Scenes.currentScene().changeVariant(1); + + case UP -> + Scenes.currentScene().changeVariant(-1); + default -> + System.out.println("Key pressed: " + eventKeyboard.getKey()); + } } } else if (e instanceof EventFrame) { paint(); diff --git a/examples/scenes/src/RunHandlerScene.java b/examples/scenes/src/RunHandlerScene.java index 9ec7673d..d89b2d6a 100644 --- a/examples/scenes/src/RunHandlerScene.java +++ b/examples/scenes/src/RunHandlerScene.java @@ -15,6 +15,13 @@ public class RunHandlerScene extends Scene { public RunHandlerScene() { lato36 = new Font(Typeface.makeFromFile(file("fonts/Lato-Regular.ttf")), 36); inter9 = new Font(inter, 9); + + _variants = new String[] { + "Approximate All", + "Approximate Spaces", + "Approximate Punctuation", + "Approximate None", + }; } @Override @@ -26,15 +33,22 @@ public void draw(Canvas canvas, int width, int height, float dpi, int xpos, int var tbHandler = new TextBlobBuilderRunHandler(text, new Point(0, 0)); var handler = new DebugTextBlobHandler().withRuns();) { + var opts = switch (_variants[_variantIdx]) { + case "Approximate Spaces" -> ShapingOptions.DEFAULT.withApproximatePunctuation(false); + case "Approximate Punctuation" -> ShapingOptions.DEFAULT.withApproximateSpaces(false); + case "Approximate None" -> ShapingOptions.DEFAULT.withApproximateSpaces(false).withApproximatePunctuation(false); + default -> ShapingOptions.DEFAULT; + }; + // TextBlobBuilderRunHandler - shaper.shape(text, lato36, FontMgr.getDefault(), null, true, width - 40, tbHandler); + shaper.shape(text, lato36, opts, width - 40, tbHandler); try (var blob = tbHandler.makeBlob()) { canvas.drawTextBlob(blob, 0, 0, textFill); canvas.translate(0, blob.getBounds().getBottom() + 20); } // DebugTextBlobHandler - shaper.shape(text, lato36, FontMgr.getDefault(), null, true, width - 40, handler); + shaper.shape(text, lato36, opts, width - 40, handler); try (var blob = handler.makeBlob()) { canvas.drawTextBlob(blob, 0, 0, textFill); @@ -69,7 +83,6 @@ public void draw(Canvas canvas, int width, int height, float dpi, int xpos, int builder.appendRun(inter9, "clusters " + Arrays.toString(run.getClusters()), 0, yPos + lh * 7); try (var detailsBlob = builder.build(); ) { - // try to fit in var detailsWidth = detailsBlob.getBounds().getWidth(); var detailsHeight = detailsBlob.getBounds().getHeight(); diff --git a/examples/scenes/src/RunIteratorScene.java b/examples/scenes/src/RunIteratorScene.java index de7712cd..46a5c6bf 100644 --- a/examples/scenes/src/RunIteratorScene.java +++ b/examples/scenes/src/RunIteratorScene.java @@ -42,17 +42,17 @@ public void draw(Canvas canvas, int width, int height, float dpi, int xpos, int var bidiIter = new TrivialBidiRunIterator(text, Bidi.DIRECTION_LEFT_TO_RIGHT); var scriptIter = new TrivialScriptRunIterator(text, "latn"); var langIter = new TrivialLanguageRunIterator(text, "en-US"); - shaper.shape(text, fontIter, bidiIter, scriptIter, langIter, null, width - 40, handler); + shaper.shape(text, fontIter, bidiIter, scriptIter, langIter, ShapingOptions.DEFAULT, width - 40, handler); drawBlob(canvas, handler, "All trivial"); } try (var handler = new DebugTextBlobHandler().withRuns(); - var fontIter = new FontMgrRunIterator(text, lato36, null);) + var fontIter = new FontMgrRunIterator(text, lato36);) { var bidiIter = new TrivialBidiRunIterator(text, Bidi.DIRECTION_LEFT_TO_RIGHT); var scriptIter = new TrivialScriptRunIterator(text, "latn"); var langIter = new TrivialLanguageRunIterator(text, "en-US"); - shaper.shape(text, fontIter, bidiIter, scriptIter, langIter, null, width - 40, handler); + shaper.shape(text, fontIter, bidiIter, scriptIter, langIter, ShapingOptions.DEFAULT, width - 40, handler); drawBlob(canvas, handler, "FontMgrRunIterator"); } @@ -69,7 +69,7 @@ public void draw(Canvas canvas, int width, int height, float dpi, int xpos, int var bidiIter = new TrivialBidiRunIterator(text, Bidi.DIRECTION_LEFT_TO_RIGHT); var scriptIter = new TrivialScriptRunIterator(text, "latn"); var langIter = new TrivialLanguageRunIterator(text, "en-US"); - shaper.shape(text, fontIter, bidiIter, scriptIter, langIter, null, width - 40, handler); + shaper.shape(text, fontIter, bidiIter, scriptIter, langIter, ShapingOptions.DEFAULT, width - 40, handler); drawBlob(canvas, handler, "CustomFontRunIterator"); } @@ -79,7 +79,7 @@ public void draw(Canvas canvas, int width, int height, float dpi, int xpos, int var fontIter = new TrivialFontRunIterator(text, lato36); var scriptIter = new TrivialScriptRunIterator(text, "latn"); var langIter = new TrivialLanguageRunIterator(text, "en-US"); - shaper.shape(text, fontIter, bidiIter, scriptIter, langIter, null, width - 40, handler); + shaper.shape(text, fontIter, bidiIter, scriptIter, langIter, ShapingOptions.DEFAULT, width - 40, handler); drawBlob(canvas, handler, "IcuBidiRunIterator"); } @@ -89,7 +89,7 @@ public void draw(Canvas canvas, int width, int height, float dpi, int xpos, int var bidiIter = new JavaTextBidiRunIterator(text); var scriptIter = new TrivialScriptRunIterator(text, "latn"); var langIter = new TrivialLanguageRunIterator(text, "en-US"); - shaper.shape(text, fontIter, bidiIter, scriptIter, langIter, null, width - 40, handler); + shaper.shape(text, fontIter, bidiIter, scriptIter, langIter, ShapingOptions.DEFAULT, width - 40, handler); drawBlob(canvas, handler, "JavaTextBidiRunIterator"); } @@ -99,17 +99,17 @@ public void draw(Canvas canvas, int width, int height, float dpi, int xpos, int var fontIter = new TrivialFontRunIterator(text, lato36); var bidiIter = new TrivialBidiRunIterator(text, Bidi.DIRECTION_LEFT_TO_RIGHT); var langIter = new TrivialLanguageRunIterator(text, "en-US"); - shaper.shape(text, fontIter, bidiIter, scriptIter, langIter, null, width - 40, handler); + shaper.shape(text, fontIter, bidiIter, scriptIter, langIter, ShapingOptions.DEFAULT, width - 40, handler); drawBlob(canvas, handler, "HbIcuScriptRunIterator"); } try (var handler = new DebugTextBlobHandler().withRuns(); - var fontIter = new FontMgrRunIterator(text, lato36, null); + var fontIter = new FontMgrRunIterator(text, lato36); var bidiIter = new IcuBidiRunIterator(text, Bidi.DIRECTION_LEFT_TO_RIGHT); var scriptIter = new HbIcuScriptRunIterator(text);) { var langIter = new TrivialLanguageRunIterator(text, "en-US"); - shaper.shape(text, fontIter, bidiIter, scriptIter, langIter, null, width - 40, handler); + shaper.shape(text, fontIter, bidiIter, scriptIter, langIter, ShapingOptions.DEFAULT, width - 40, handler); drawBlob(canvas, handler, "All native"); } } diff --git a/examples/scenes/src/Scenes.java b/examples/scenes/src/Scenes.java index 32ee174c..fa695f42 100644 --- a/examples/scenes/src/Scenes.java +++ b/examples/scenes/src/Scenes.java @@ -6,7 +6,7 @@ public class Scenes { public static TreeMap scenes; - public static String currentScene = "Runtime Effect"; + public static String currentScene = "Run Handler"; public static HUD hud = new HUD(); public static boolean stats = true; diff --git a/examples/scenes/src/ShapersScene.java b/examples/scenes/src/ShapersScene.java index 9ebcf09b..6dd121ad 100644 --- a/examples/scenes/src/ShapersScene.java +++ b/examples/scenes/src/ShapersScene.java @@ -17,31 +17,31 @@ public void drawWithShaper(Canvas canvas, float maxWidth, int color, String name fill.setColor(color); String text = "Incomprehensibilities word word word word ararar123456 +++ <-> *** c̝̣̱̲͈̝ͨ͐̈ͪͨ̃ͥͅh̙̬̿̂a̯͎͍̜͐͌͂̚o̬s͉̰͊̀ " + name; - try (var blob = shaper.shape(text, firaCode11, null, true, maxWidth, new Point(0, 0)); ) { + try (var blob = shaper.shape(text, firaCode11, ShapingOptions.DEFAULT, maxWidth, new Point(0, 0)); ) { canvas.drawTextBlob(blob, 0, 0, fill); canvas.translate(0, blob.getBounds().getHeight()); } - try (var blob = shaper.shape(text + " RTL", firaCode11, null, false, maxWidth, new Point(0, 0)); ) { + try (var blob = shaper.shape(text + " RTL", firaCode11, ShapingOptions.DEFAULT.withLeftToRight(false), maxWidth, new Point(0, 0)); ) { canvas.drawTextBlob(blob, 0, 0, fill); canvas.translate(0, blob.getBounds().getHeight()); } String features = "ss01 cv01 onum -calt"; - try (var blob = shaper.shape(text + " " + features, firaCode11, FontFeature.parse(features), true, maxWidth, new Point(0, 0)); ) { + try (var blob = shaper.shape(text + " " + features, firaCode11, ShapingOptions.DEFAULT.withFeatures(features), maxWidth, new Point(0, 0)); ) { canvas.drawTextBlob(blob, 0, 0, fill); canvas.translate(0, blob.getBounds().getHeight()); } features = "ss01[42:45] cv01[42:45] onum[48:51] calt[59:60]=0"; - try (var blob = shaper.shape(text + " " + features, firaCode11, FontFeature.parse(features), true, maxWidth, new Point(0, 0)); ) { + try (var blob = shaper.shape(text + " " + features, firaCode11, ShapingOptions.DEFAULT.withFeatures(features), maxWidth, new Point(0, 0)); ) { canvas.drawTextBlob(blob, 0, 0, fill); canvas.translate(0, blob.getBounds().getHeight()); } float height = 0; - try (var blob = shaper.shape(text, firaCode11, null, true, 75, new Point(0, 0)); ) { + try (var blob = shaper.shape(text, firaCode11, ShapingOptions.DEFAULT, 75, new Point(0, 0)); ) { canvas.drawTextBlob(blob, 0, 0, fill); Rect bounds = blob.getBounds(); canvas.drawRect(Rect.makeXYWH(0, 0, 75, bounds.getHeight()), stroke); diff --git a/examples/scenes/src/TextShapeBenchScene.java b/examples/scenes/src/TextShapeBenchScene.java index f55bd204..b36cd988 100644 --- a/examples/scenes/src/TextShapeBenchScene.java +++ b/examples/scenes/src/TextShapeBenchScene.java @@ -18,11 +18,11 @@ public TextShapeBenchScene() { _variants = new String[] { "Tabs Paragraph", "Tabs Paragraph No-Cache", "Tabs TextLine", "Emoji Paragraph", "Emoji Paragraph No-Cache", "Emoji TextLine", - "Greek Paragraph", "Greek Paragraph No-Cache", "Greek TextLine", + "Greek Paragraph", "Greek Paragraph No-Cache", "Greek TextLine", "Greek TextLine No-Approx", "Notdef Paragraph", "Notdef Paragraph No-Cache", "Notdef TextLine", "English Paragraph", "English Paragraph No-Cache", "English TextLine", }; - _variantIdx = 5; + _variantIdx = 9; font = new Font(jbMono, fontSize).setSubpixel(true); metrics = font.getMetrics(); @@ -67,12 +67,15 @@ else if ("English".equals(variant[0])) } } } - } else { + } else { // TextLine try (var shaper = Shaper.makeShapeDontWrapOrReorder();) { for (int i = 1; true; ++i) { float y = i * padding; if (y > height - padding) break; - try (var line = shaper.shapeLine(i + " [" + text + "]", font, null, true);) { + try (var line = variant.length > 2 // No-Approx + ? shaper.shapeLine(i + " [" + text + "]", font, ShapingOptions.DEFAULT.withApproximateSpaces(false).withApproximatePunctuation(false)) + : shaper.shapeLine(i + " [" + text + "]", font)) + { canvas.drawTextLine(line, padding, y - metrics.getAscent(), blackFill); canvas.drawRect(Rect.makeXYWH(padding, y, line.getWidth(), metrics.getHeight()), redStroke); for (float x: TextLine._nGetRunPositions(line._ptr)) diff --git a/examples/scenes/src/WallOfTextScene.java b/examples/scenes/src/WallOfTextScene.java index 6723ddb0..190194e6 100644 --- a/examples/scenes/src/WallOfTextScene.java +++ b/examples/scenes/src/WallOfTextScene.java @@ -92,7 +92,7 @@ void drawParagraph(Canvas canvas, float fontSize, float padding, float textWidth TextBlob makeBlob(String text, Shaper shaper, float textWidth) { if (_variants[_variantIdx].startsWith("JVM RunHandler")) { try (var handler = new DebugTextBlobHandler();) { - shaper.shape(text, font, FontMgr.getDefault(), null, true, textWidth, handler); + shaper.shape(text, font, ShapingOptions.DEFAULT, textWidth, handler); return handler.makeBlob(); } } else diff --git a/platform/cc/interop.cc b/platform/cc/interop.cc index 4b76bd74..e61b2757 100644 --- a/platform/cc/interop.cc +++ b/platform/cc/interop.cc @@ -296,6 +296,19 @@ namespace skija { } } + namespace FontMgr { + jclass cls; + + void onLoad(JNIEnv* env) { + jclass local = env->FindClass("org/jetbrains/skija/FontMgr"); + cls = static_cast(env->NewGlobalRef(local)); + } + + void onUnload(JNIEnv* env) { + env->DeleteGlobalRef(cls); + } + } + namespace FontStyle { SkFontStyle fromJava(jint style) { return SkFontStyle(style & 0xFFFF, (style >> 16) & 0xFF, static_cast((style >> 24) & 0xFF)); diff --git a/platform/cc/interop.hh b/platform/cc/interop.hh index e4e8e5b9..cc47d9cb 100644 --- a/platform/cc/interop.hh +++ b/platform/cc/interop.hh @@ -152,6 +152,12 @@ namespace skija { jobject toJava(JNIEnv* env, const SkFontMetrics& m); } + namespace FontMgr { + extern jclass cls; + void onLoad(JNIEnv* env); + void onUnload(JNIEnv* env); + } + namespace FontStyle { SkFontStyle fromJava(jint style); jint toJava(const SkFontStyle& fs); diff --git a/platform/cc/shaper/FontMgrRunIterator.cc b/platform/cc/shaper/FontMgrRunIterator.cc index f1609dc1..a4ee3a6a 100644 --- a/platform/cc/shaper/FontMgrRunIterator.cc +++ b/platform/cc/shaper/FontMgrRunIterator.cc @@ -6,15 +6,25 @@ #include "SkShaper.h" extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skija_shaper_FontMgrRunIterator__1nMake - (JNIEnv* env, jclass jclass, jlong textPtr, jlong fontPtr, jlong fontMgrPtr) { + (JNIEnv* env, jclass jclass, jlong textPtr, jlong fontPtr, jobject opts) { SkString* text = reinterpret_cast(static_cast(textPtr)); SkFont* font = reinterpret_cast(static_cast(fontPtr)); - sk_sp fontMgr = fontMgrPtr == 0 ? SkFontMgr::RefDefault() : sk_ref_sp(reinterpret_cast(static_cast(fontMgrPtr))); - + jobject fontMgrObj = env->GetObjectField(opts, skija::shaper::ShapingOptions::_fontMgr); + sk_sp fontMgr = fontMgrObj == nullptr + ? SkFontMgr::RefDefault() + : sk_ref_sp(reinterpret_cast(skija::impl::Native::fromJava(env, fontMgrObj, skija::FontMgr::cls))); std::shared_ptr graphemeIter = skija::shaper::graphemeBreakIterator(*text); if (!graphemeIter) return 0; - auto instance = new FontRunIterator(text->c_str(), text->size(), *font, fontMgr, graphemeIter); + auto instance = new FontRunIterator( + text->c_str(), + text->size(), + *font, + fontMgr, + graphemeIter, + env->GetBooleanField(opts, skija::shaper::ShapingOptions::_approximateSpaces), + env->GetBooleanField(opts, skija::shaper::ShapingOptions::_approximatePunctuation) + ); return reinterpret_cast(instance); } diff --git a/platform/cc/shaper/FontRunIterator.cc b/platform/cc/shaper/FontRunIterator.cc index 9d29076c..c6aac066 100644 --- a/platform/cc/shaper/FontRunIterator.cc +++ b/platform/cc/shaper/FontRunIterator.cc @@ -60,7 +60,11 @@ void FontRunIterator::consume() { SkUnichar u = utf8_next(&ptr, clusterEnd); // Do not switch font on control, whitespace or punct - if (ptr == clusterEnd && fCurrentFont->getTypeface()->unicharToGlyph(u) != 0 && (u_iscntrl(u) || u_isWhitespace(u) || u_ispunct(u))) + if (ptr == clusterEnd + && fCurrentFont->getTypeface()->unicharToGlyph(u) != 0 + && (u_iscntrl(u) + || (fApproximateSpaces && u_isWhitespace(u)) + || (fApproximatePunctuation && u_ispunct(u)))) continue; // End run if not using initial typeface and initial typeface has this character. diff --git a/platform/cc/shaper/FontRunIterator.hh b/platform/cc/shaper/FontRunIterator.hh index e2104439..e9bb60d4 100644 --- a/platform/cc/shaper/FontRunIterator.hh +++ b/platform/cc/shaper/FontRunIterator.hh @@ -12,7 +12,9 @@ public: const char* requestName, SkFontStyle requestStyle, const SkShaper::LanguageRunIterator* lang, - std::shared_ptr graphemeIter) + std::shared_ptr graphemeIter, + bool approximateSpaces, + bool approximatePunctuation) : fCurrent(utf8) , fBegin(utf8) , fEnd(fCurrent + utf8Bytes) @@ -24,13 +26,30 @@ public: , fRequestStyle(requestStyle) , fLanguage(lang) , fGraphemeIter(graphemeIter) + , fApproximateSpaces(approximateSpaces) + , fApproximatePunctuation(approximatePunctuation) { fFont.setTypeface(font.refTypefaceOrDefault()); fFallbackFont.setTypeface(nullptr); } - FontRunIterator(const char* utf8, size_t utf8Bytes, const SkFont& font, sk_sp fallbackMgr, std::shared_ptr graphemeIter) - : FontRunIterator(utf8, utf8Bytes, font, std::move(fallbackMgr), nullptr, font.refTypefaceOrDefault()->fontStyle(), nullptr, graphemeIter) { + FontRunIterator(const char* utf8, + size_t utf8Bytes, + const SkFont& font, + sk_sp fallbackMgr, + std::shared_ptr graphemeIter, + bool approximateSpaces, + bool approximatePunctuation) + : FontRunIterator(utf8, + utf8Bytes, + font, + std::move(fallbackMgr), + nullptr, + font.refTypefaceOrDefault()->fontStyle(), + nullptr, + graphemeIter, + approximateSpaces, + approximatePunctuation) { } ~FontRunIterator() { @@ -62,4 +81,6 @@ private: SkFontStyle const fRequestStyle; SkShaper::LanguageRunIterator const * const fLanguage; std::shared_ptr fGraphemeIter; + bool fApproximateSpaces; + bool fApproximatePunctuation; }; \ No newline at end of file diff --git a/platform/cc/shaper/Shaper.cc b/platform/cc/shaper/Shaper.cc index 69e7b46d..830b7b2c 100644 --- a/platform/cc/shaper/Shaper.cc +++ b/platform/cc/shaper/Shaper.cc @@ -56,18 +56,26 @@ extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skija_shaper_Shaper__1nMak } extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skija_shaper_Shaper__1nShapeBlob - (JNIEnv* env, jclass jclass, jlong ptr, jstring textObj, jlong fontPtr, jobjectArray featuresArr, jboolean leftToRight, jfloat width, jfloat offsetX, jfloat offsetY) { + (JNIEnv* env, jclass jclass, jlong ptr, jstring textObj, jlong fontPtr, jobject opts, jfloat width, jfloat offsetX, jfloat offsetY) { SkShaper* instance = reinterpret_cast(static_cast(ptr)); SkString text = skString(env, textObj); std::shared_ptr graphemeIter = skija::shaper::graphemeBreakIterator(text); if (!graphemeIter) return 0; SkFont* font = reinterpret_cast(static_cast(fontPtr)); - std::vector features = skija::FontFeature::fromJavaArray(env, featuresArr); - - std::unique_ptr fontRunIter(new FontRunIterator(text.c_str(), text.size(), *font, SkFontMgr::RefDefault(), graphemeIter)); + std::vector features = skija::shaper::ShapingOptions::getFeatures(env, opts); + + std::unique_ptr fontRunIter(new FontRunIterator( + text.c_str(), + text.size(), + *font, + SkFontMgr::RefDefault(), + graphemeIter, + env->GetBooleanField(opts, skija::shaper::ShapingOptions::_approximateSpaces), + env->GetBooleanField(opts, skija::shaper::ShapingOptions::_approximatePunctuation) + )); if (!fontRunIter) return 0; - uint8_t defaultBiDiLevel = leftToRight ? UBIDI_DEFAULT_LTR : UBIDI_DEFAULT_RTL; + uint8_t defaultBiDiLevel = env->GetBooleanField(opts, skija::shaper::ShapingOptions::_leftToRight) ? UBIDI_DEFAULT_LTR : UBIDI_DEFAULT_RTL; std::unique_ptr bidiRunIter(SkShaper::MakeBiDiRunIterator(text.c_str(), text.size(), defaultBiDiLevel)); if (!bidiRunIter) return 0; @@ -85,7 +93,7 @@ extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skija_shaper_Shaper__1nSha } extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skija_shaper_Shaper__1nShapeLine - (JNIEnv* env, jclass jclass, jlong ptr, jstring textObj, jlong fontPtr, jobjectArray featuresArr, jboolean leftToRight) { + (JNIEnv* env, jclass jclass, jlong ptr, jstring textObj, jlong fontPtr, jobject opts) { SkShaper* instance = reinterpret_cast(static_cast(ptr)); SkString text = skString(env, textObj); @@ -93,12 +101,19 @@ extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skija_shaper_Shaper__1nSha if (!graphemeIter) return 0; SkFont* font = reinterpret_cast(static_cast(fontPtr)); - std::vector features = skija::FontFeature::fromJavaArray(env, featuresArr); - - std::unique_ptr fontRunIter(new FontRunIterator(text.c_str(), text.size(), *font, SkFontMgr::RefDefault(), graphemeIter)); + std::vector features = skija::shaper::ShapingOptions::getFeatures(env, opts); + + std::unique_ptr fontRunIter(new FontRunIterator( + text.c_str(), + text.size(), + *font, + SkFontMgr::RefDefault(), + graphemeIter, + env->GetBooleanField(opts, skija::shaper::ShapingOptions::_approximateSpaces), + env->GetBooleanField(opts, skija::shaper::ShapingOptions::_approximatePunctuation))); if (!fontRunIter) return 0; - uint8_t defaultBiDiLevel = leftToRight ? UBIDI_DEFAULT_LTR : UBIDI_DEFAULT_RTL; + uint8_t defaultBiDiLevel = env->GetBooleanField(opts, skija::shaper::ShapingOptions::_leftToRight) ? UBIDI_DEFAULT_LTR : UBIDI_DEFAULT_RTL; std::unique_ptr bidiRunIter(SkShaper::MakeBiDiRunIterator(text.c_str(), text.size(), defaultBiDiLevel)); if (!bidiRunIter) return 0; @@ -314,8 +329,7 @@ class SkijaRunHandler: public SkShaper::RunHandler { }; extern "C" JNIEXPORT void JNICALL Java_org_jetbrains_skija_shaper_Shaper__1nShape - (JNIEnv* env, jclass jclass, jlong ptr, jlong textPtr, jobject fontRunIterObj, jobject bidiRunIterObj, jobject scriptRunIterObj, jobject languageRunIterObj, - jobjectArray featuresArr, jfloat width, jobject runHandlerObj) + (JNIEnv* env, jclass jclass, jlong ptr, jlong textPtr, jobject fontRunIterObj, jobject bidiRunIterObj, jobject scriptRunIterObj, jobject languageRunIterObj, jobject opts, jfloat width, jobject runHandlerObj) { SkShaper* instance = reinterpret_cast(static_cast(ptr)); SkString* text = reinterpret_cast(static_cast(textPtr)); @@ -337,7 +351,7 @@ extern "C" JNIEXPORT void JNICALL Java_org_jetbrains_skija_shaper_Shaper__1nShap auto languageRunIter = SkijaLanguageRunIterator(env, languageRunIterObj, *text); - std::vector features = skija::FontFeature::fromJavaArray(env, featuresArr); + std::vector features = skija::shaper::ShapingOptions::getFeatures(env, opts); auto nativeRunHandler = (SkShaper::RunHandler*) skija::impl::Native::fromJava(env, runHandlerObj, skija::shaper::TextBlobBuilderRunHandler::cls); std::unique_ptr localRunHandler; diff --git a/platform/cc/shaper/interop.cc b/platform/cc/shaper/interop.cc index 671d8f53..8cdc8b9f 100644 --- a/platform/cc/shaper/interop.cc +++ b/platform/cc/shaper/interop.cc @@ -153,6 +153,27 @@ namespace skija { } } + namespace ShapingOptions { + jfieldID _fontMgr; + jfieldID _features; + jfieldID _leftToRight; + jfieldID _approximateSpaces; + jfieldID _approximatePunctuation; + + void onLoad(JNIEnv* env) { + jclass cls = env->FindClass("org/jetbrains/skija/shaper/ShapingOptions"); + _fontMgr = env->GetFieldID(cls, "_fontMgr", "Lorg/jetbrains/skija/FontMgr;"); + _features = env->GetFieldID(cls, "_features", "[Lorg/jetbrains/skija/FontFeature;"); + _leftToRight = env->GetFieldID(cls, "_leftToRight", "Z"); + _approximateSpaces = env->GetFieldID(cls, "_approximateSpaces", "Z"); + _approximatePunctuation = env->GetFieldID(cls, "_approximatePunctuation", "Z"); + } + + std::vector getFeatures(JNIEnv* env, jobject opts) { + return skija::FontFeature::fromJavaArray(env, (jobjectArray) env->GetObjectField(opts, _features)); + } + } + namespace TextBlobBuilderRunHandler { jclass cls; @@ -176,6 +197,7 @@ namespace skija { RunHandler::onLoad(env); RunInfo::onLoad(env); ScriptRun::onLoad(env); + ShapingOptions::onLoad(env); TextBlobBuilderRunHandler::onLoad(env); SkLoadICU(); diff --git a/platform/cc/shaper/interop.hh b/platform/cc/shaper/interop.hh index 97b1f32d..db10248e 100644 --- a/platform/cc/shaper/interop.hh +++ b/platform/cc/shaper/interop.hh @@ -80,6 +80,17 @@ namespace skija { void onLoad(JNIEnv* env); } + namespace ShapingOptions { + extern jfieldID _fontMgr; + extern jfieldID _features; + extern jfieldID _leftToRight; + extern jfieldID _approximateSpaces; + extern jfieldID _approximatePunctuation; + + void onLoad(JNIEnv* env); + std::vector getFeatures(JNIEnv* env, jobject opts); + } + namespace TextBlobBuilderRunHandler { extern jclass cls; diff --git a/script/clean.py b/script/clean.py index b2078723..e60de0f8 100755 --- a/script/clean.py +++ b/script/clean.py @@ -7,6 +7,11 @@ def main(): shutil.rmtree('platform/build', ignore_errors = True) shutil.rmtree('platform/target', ignore_errors = True) shutil.rmtree('tests/target', ignore_errors = True) + shutil.rmtree('examples/lwjgl/target', ignore_errors = True) + shutil.rmtree('examples/kwinit/target', ignore_errors = True) + shutil.rmtree('examples/jwm/target', ignore_errors = True) + shutil.rmtree('examples/swt/target', ignore_errors = True) + return 0 if __name__ == '__main__': diff --git a/script/common.py b/script/common.py index d8b9f620..1ff16d48 100755 --- a/script/common.py +++ b/script/common.py @@ -52,7 +52,7 @@ def deps(): fetch_maven('org.jetbrains', 'annotations', '20.1.0'), ] -def javac(sources, target, classpath = [], modulepath = [], add_modules = [], release = '11'): +def javac(sources, target, classpath = [], modulepath = [], add_modules = [], release = '11', opts = []): classes = {path.stem: path.stat().st_mtime for path in pathlib.Path(target).rglob('*.class') if '$' not in path.stem} newer = lambda path: path.stem not in classes or path.stat().st_mtime > classes.get(path.stem) new_sources = [path for path in sources if newer(pathlib.Path(path))] @@ -61,7 +61,7 @@ def javac(sources, target, classpath = [], modulepath = [], add_modules = [], re check_call([ 'javac', '-encoding', 'UTF8', - '--release', release, + '--release', release] + opts + [ # '-J--illegal-access=permit', # '-Xlint:deprecation', # '-Xlint:unchecked', diff --git a/shared/java/AnimationDisposalMethod.java b/shared/java/AnimationDisposalMode.java similarity index 87% rename from shared/java/AnimationDisposalMethod.java rename to shared/java/AnimationDisposalMode.java index efb7c6a6..e337fcb5 100644 --- a/shared/java/AnimationDisposalMethod.java +++ b/shared/java/AnimationDisposalMode.java @@ -8,7 +8,7 @@ * *

Names are based on the GIF 89a spec.

*/ -public enum AnimationDisposalMethod { +public enum AnimationDisposalMode { @ApiStatus.Internal _UNUSED, @@ -34,5 +34,5 @@ public enum AnimationDisposalMethod { */ RESTORE_PREVIOUS; - @ApiStatus.Internal public static final AnimationDisposalMethod[] _values = values(); + @ApiStatus.Internal public static final AnimationDisposalMode[] _values = values(); } \ No newline at end of file diff --git a/shared/java/AnimationFrameInfo.java b/shared/java/AnimationFrameInfo.java index ed42a4a3..520072c8 100644 --- a/shared/java/AnimationFrameInfo.java +++ b/shared/java/AnimationFrameInfo.java @@ -10,7 +10,7 @@ public class AnimationFrameInfo { @ApiStatus.Internal public AnimationFrameInfo(int requiredFrame, int duration, boolean fullyReceived, int alphaTypeOrdinal, boolean hasAlphaWithinBounds, int disposalMethodOrdinal, int blendModeOrdinal, IRect frameRect) { - this(requiredFrame, duration, fullyReceived, ColorAlphaType._values[alphaTypeOrdinal], hasAlphaWithinBounds, AnimationDisposalMethod._values[disposalMethodOrdinal], BlendMode._values[blendModeOrdinal], frameRect); + this(requiredFrame, duration, fullyReceived, ColorAlphaType._values[alphaTypeOrdinal], hasAlphaWithinBounds, AnimationDisposalMode._values[disposalMethodOrdinal], BlendMode._values[blendModeOrdinal], frameRect); } /** @@ -20,7 +20,7 @@ public AnimationFrameInfo(int requiredFrame, int duration, boolean fullyReceived * *

Note that this is the *earliest* frame that can be used * for blending. Any frame from [_requiredFrame, i) can be - * used, unless its getDisposalMethod() is {@link AnimationDisposalMethod#RESTORE_PREVIOUS}.

+ * used, unless its getDisposalMethod() is {@link AnimationDisposalMode#RESTORE_PREVIOUS}.

*/ @ApiStatus.Internal public int _requiredFrame; @@ -62,7 +62,7 @@ public AnimationFrameInfo(int requiredFrame, int duration, boolean fullyReceived *

How this frame should be modified before decoding the next one.

*/ @ApiStatus.Internal - public AnimationDisposalMethod _disposalMethod; + public AnimationDisposalMode _disposalMethod; /** *

How this frame should blend with the prior frame.

diff --git a/shared/java/TextLine.java b/shared/java/TextLine.java index caab8fcb..790ba617 100644 --- a/shared/java/TextLine.java +++ b/shared/java/TextLine.java @@ -20,13 +20,13 @@ public static class _FinalizerHolder { @NotNull @Contract("_, _ -> new") public static TextLine make(String text, Font font) { - return make(text, font, null, true); + return make(text, font, ShapingOptions.DEFAULT); } @NotNull @Contract("_, _, _, _ -> new") - public static TextLine make(String text, Font font, @Nullable FontFeature[] features, boolean leftToRight) { + public static TextLine make(String text, Font font, ShapingOptions opts) { try (Shaper shaper = Shaper.makeShapeDontWrapOrReorder();) { - return shaper.shapeLine(text, font, features, leftToRight); + return shaper.shapeLine(text, font, opts); } } diff --git a/shared/java/shaper/FontMgrRunIterator.java b/shared/java/shaper/FontMgrRunIterator.java index 525273f5..03cd598a 100644 --- a/shared/java/shaper/FontMgrRunIterator.java +++ b/shared/java/shaper/FontMgrRunIterator.java @@ -8,16 +8,20 @@ public class FontMgrRunIterator extends ManagedRunIterator { static { Library.staticLoad(); } - public FontMgrRunIterator(ManagedString text, boolean manageText, Font font, FontMgr fontMgr) { - super(_nMake(Native.getPtr(text), Native.getPtr(font), Native.getPtr(fontMgr)), text, manageText); + public FontMgrRunIterator(ManagedString text, boolean manageText, Font font, @NotNull ShapingOptions opts) { + super(_nMake(Native.getPtr(text), Native.getPtr(font), opts), text, manageText); Stats.onNativeCall(); Reference.reachabilityFence(text); Reference.reachabilityFence(font); - Reference.reachabilityFence(fontMgr); + Reference.reachabilityFence(opts); } - public FontMgrRunIterator(String text, Font font, @Nullable FontMgr fontMgr) { - this(new ManagedString(text), true, font, fontMgr); + public FontMgrRunIterator(String text, Font font, @NotNull ShapingOptions opts) { + this(new ManagedString(text), true, font, opts); + } + + public FontMgrRunIterator(String text, Font font) { + this(new ManagedString(text), true, font, ShapingOptions.DEFAULT); } @Override @@ -30,6 +34,6 @@ public FontRun next() { } } - @ApiStatus.Internal public static native long _nMake(long textPtr, long fontPtr, long fontMgrPtr); + @ApiStatus.Internal public static native long _nMake(long textPtr, long fontPtr, ShapingOptions opts); @ApiStatus.Internal public static native long _nGetCurrentFont(long ptr); } \ No newline at end of file diff --git a/shared/java/shaper/Shaper.java b/shared/java/shaper/Shaper.java index 042cbbf1..d73d74da 100644 --- a/shared/java/shaper/Shaper.java +++ b/shared/java/shaper/Shaper.java @@ -97,29 +97,25 @@ public static Shaper make(@Nullable FontMgr fontMgr) { @Nullable @Contract("_, _ -> new") public TextBlob shape(String text, Font font) { - return shape(text, font, null, true, Float.POSITIVE_INFINITY, Point.ZERO); + return shape(text, font, ShapingOptions.DEFAULT, Float.POSITIVE_INFINITY, Point.ZERO); } @Nullable @Contract("_, _, _ -> new") public TextBlob shape(String text, Font font, float width) { - return shape(text, font, null, true, width, Point.ZERO); + return shape(text, font, ShapingOptions.DEFAULT, width, Point.ZERO); } @Nullable @Contract("_, _, _, _ -> new") public TextBlob shape(String text, Font font, float width, @NotNull Point offset) { - return shape(text, font, null, true, width, offset); + return shape(text, font, ShapingOptions.DEFAULT, width, offset); } @Nullable @Contract("_, _, _, _, _ -> new") - public TextBlob shape(String text, Font font, boolean leftToRight, float width, @NotNull Point offset) { - return shape(text, font, null, leftToRight, width, offset); - } - - @Nullable @Contract("_, _, _, _, _, _ -> new") - public TextBlob shape(String text, Font font, @Nullable FontFeature[] features, boolean leftToRight, float width, @NotNull Point offset) { + public TextBlob shape(String text, Font font, @NotNull ShapingOptions opts, float width, @NotNull Point offset) { try { + assert opts != null : "Can’t Shaper::shape with opts == null"; Stats.onNativeCall(); - long ptr = _nShapeBlob(_ptr, text, Native.getPtr(font), features, leftToRight, width, offset._x, offset._y); + long ptr = _nShapeBlob(_ptr, text, Native.getPtr(font), opts, width, offset._x, offset._y); return 0 == ptr ? null : new TextBlob(ptr); } finally { Reference.reachabilityFence(this); @@ -127,22 +123,20 @@ public TextBlob shape(String text, Font font, @Nullable FontFeature[] features, } } - @NotNull @Contract("_, _, _, _, _, _, _ -> this") + @NotNull @Contract("_, _, _, _, _ -> this") public Shaper shape(String text, Font font, - @Nullable FontMgr fontMgr, - @Nullable FontFeature[] features, - boolean leftToRight, + @NotNull ShapingOptions opts, float width, RunHandler runHandler) { try (ManagedString textUtf8 = new ManagedString(text); - FontMgrRunIterator fontIter = new FontMgrRunIterator(textUtf8, false, font, fontMgr); - IcuBidiRunIterator bidiIter = new IcuBidiRunIterator(textUtf8, false, leftToRight ? java.text.Bidi.DIRECTION_LEFT_TO_RIGHT : java.text.Bidi.DIRECTION_RIGHT_TO_LEFT); + FontMgrRunIterator fontIter = new FontMgrRunIterator(textUtf8, false, font, opts); + IcuBidiRunIterator bidiIter = new IcuBidiRunIterator(textUtf8, false, opts._leftToRight ? java.text.Bidi.DIRECTION_LEFT_TO_RIGHT : java.text.Bidi.DIRECTION_RIGHT_TO_LEFT); HbIcuScriptRunIterator scriptIter = new HbIcuScriptRunIterator(textUtf8, false);) { Iterator langIter = new TrivialLanguageRunIterator(text, Locale.getDefault().toLanguageTag()); - return shape(textUtf8, fontIter, bidiIter, scriptIter, langIter, features, width, runHandler); + return shape(textUtf8, fontIter, bidiIter, scriptIter, langIter, opts, width, runHandler); } } @@ -152,12 +146,12 @@ public Shaper shape(@NotNull String text, @NotNull Iterator bidiIter, @NotNull Iterator scriptIter, @NotNull Iterator langIter, - @Nullable FontFeature[] features, + @NotNull ShapingOptions opts, float width, @NotNull RunHandler runHandler) { try (ManagedString textUtf8 = new ManagedString(text);) { - return shape(textUtf8, fontIter, bidiIter, scriptIter, langIter, features, width, runHandler); + return shape(textUtf8, fontIter, bidiIter, scriptIter, langIter, opts, width, runHandler); } } @@ -167,7 +161,7 @@ public Shaper shape(@NotNull ManagedString textUtf8, @NotNull Iterator bidiIter, @NotNull Iterator scriptIter, @NotNull Iterator langIter, - @Nullable FontFeature[] features, + @NotNull ShapingOptions opts, float width, @NotNull RunHandler runHandler) { @@ -175,22 +169,29 @@ public Shaper shape(@NotNull ManagedString textUtf8, assert bidiIter != null : "BidiRunIterator == null"; assert scriptIter != null : "ScriptRunIterator == null"; assert langIter != null : "LanguageRunIterator == null"; + assert opts != null : "Can’t Shaper::shape with opts == null"; Stats.onNativeCall(); - _nShape(_ptr, Native.getPtr(textUtf8), fontIter, bidiIter, scriptIter, langIter, features, width, runHandler); + _nShape(_ptr, Native.getPtr(textUtf8), fontIter, bidiIter, scriptIter, langIter, opts, width, runHandler); return this; } - @NotNull @Contract("_, _, _, _, _, _ -> new") - public TextLine shapeLine(String text, Font font, @Nullable FontFeature[] features, boolean leftToRight) { + @NotNull @Contract("_, _, _ -> new") + public TextLine shapeLine(String text, Font font, @NotNull ShapingOptions opts) { try { + assert opts != null : "Can’t Shaper::shapeLine with opts == null"; Stats.onNativeCall(); - return new TextLine(_nShapeLine(_ptr, text, Native.getPtr(font), features, leftToRight)); + return new TextLine(_nShapeLine(_ptr, text, Native.getPtr(font), opts)); } finally { Reference.reachabilityFence(this); Reference.reachabilityFence(font); } } + @NotNull @Contract("_, _, _ -> new") + public TextLine shapeLine(String text, Font font) { + return shapeLine(text, font, ShapingOptions.DEFAULT); + } + @ApiStatus.Internal public Shaper(long ptr) { super(ptr, _FinalizerHolder.PTR); @@ -208,8 +209,7 @@ public static class _FinalizerHolder { public static native long _nMakeShapeDontWrapOrReorder(long fontMgrPtr); public static native long _nMakeCoreText(); public static native long _nMake(long fontMgrPtr); - public static native long _nShapeBlob(long ptr, String text, long fontPtr, FontFeature[] features, boolean leftToRight, float width, float offsetX, float offsetY); - public static native long _nShapeLine(long ptr, String text, long fontPtr, FontFeature[] features, boolean leftToRight); - public static native void _nShape(long ptr, long textPtr, Iterator fontIter, Iterator bidiIter, Iterator scriptIter, Iterator langIter, - FontFeature[] features, float width, RunHandler runHandler); + public static native long _nShapeBlob(long ptr, String text, long fontPtr, ShapingOptions opts, float width, float offsetX, float offsetY); + public static native long _nShapeLine(long ptr, String text, long fontPtr, ShapingOptions opts); + public static native void _nShape(long ptr, long textPtr, Iterator fontIter, Iterator bidiIter, Iterator scriptIter, Iterator langIter, ShapingOptions opts, float width, RunHandler runHandler); } diff --git a/shared/java/shaper/ShapingOptions.java b/shared/java/shaper/ShapingOptions.java new file mode 100644 index 00000000..52f807f0 --- /dev/null +++ b/shared/java/shaper/ShapingOptions.java @@ -0,0 +1,42 @@ +package org.jetbrains.skija.shaper; + +import lombok.*; +import lombok.Data; +import org.jetbrains.annotations.*; +import org.jetbrains.skija.*; + +@Data @With +public class ShapingOptions { + public static final ShapingOptions DEFAULT = new ShapingOptions(null, null, true, true, true); + + @ApiStatus.Internal @Nullable + public final FontMgr _fontMgr; + + @ApiStatus.Internal @Nullable + public final FontFeature[] _features; + + @ApiStatus.Internal + public final boolean _leftToRight; + + /** + * If enabled, fallback font runs will not be broken by whitespace from original font + */ + @ApiStatus.Internal + public final boolean _approximateSpaces; + + /** + * If enabled, fallback font runs will not be broken by punctuation from original font + */ + @ApiStatus.Internal + public final boolean _approximatePunctuation; + + @NotNull + public ShapingOptions withFeatures(@Nullable FontFeature[] features) { + return new ShapingOptions(_fontMgr, features, _leftToRight, _approximateSpaces, _approximatePunctuation); + } + + @NotNull + public ShapingOptions withFeatures(@Nullable String featuresString) { + return featuresString == null ? withFeatures((FontFeature[]) null) : withFeatures(FontFeature.parse(featuresString)); + } +}