mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-07-13 14:32:53 +03:00
Merge remote-tracking branch 'origin/develop' into develop
This commit is contained in:
@@ -13,7 +13,64 @@ Reference specs:
|
||||
- CSS Text Module Level 4: https://drafts.csswg.org/css-text-4/#white-space-collapsing
|
||||
- Stable TR snapshot: https://www.w3.org/TR/css-text-4/#white-space-collapsing
|
||||
|
||||
## Current State
|
||||
## Implementation Status
|
||||
|
||||
Recovered and implemented in the current workspace on 2026-06-01, without refreshing any golden
|
||||
images.
|
||||
|
||||
Implemented:
|
||||
|
||||
- Raw PCDATA is preserved when loading `UIRichText` and `UITextSpan`; CSS whitespace processing now
|
||||
happens during RichText rebuild.
|
||||
- `WhiteSpaceCollapse::Discard` exists and `white-space-collapse` parsing/stringification covers
|
||||
`collapse`, `preserve`, `preserve-breaks`, `preserve-spaces`, `break-spaces`, and `discard`.
|
||||
- `white-space` accepts the legacy values plus direct collapse keywords and `wrap`/`nowrap` tokens.
|
||||
- `UIRichText::rebuildRichText()` applies the effective collapse mode for text nodes, inline spans,
|
||||
and rebuilt `UITextSpan` roots.
|
||||
- `RichText` has a whitespace wrap mode for preserved and `break-spaces` content; `break-spaces`
|
||||
contributes preserved spaces/tabs to intrinsic width and wraps after preserved spaces/tabs.
|
||||
- CSS `tab-size` is registered as inherited with initial value `8`; `UIRichText` stores the value and
|
||||
passes it to `RichText`/`Text` measurement and wrapping.
|
||||
- Base HTML CSS now includes `pre { white-space: pre; }`.
|
||||
- Generic HTML `<pre><code>` renders as normal RichText content.
|
||||
- The old read-only `UICodeEditor` initialization is retained only when a `<pre><code>` block is
|
||||
inside `UI_TYPE_MARKDOWNVIEW`, or when `UIRichText::setUseCodeEditorForPreCodeBlocks( true )` is
|
||||
explicitly enabled. The global switch defaults to disabled.
|
||||
- Regression fixtures were restored:
|
||||
- `bin/unit_tests/assets/html/pre.code.html`
|
||||
- `bin/unit_tests/assets/html/pre.code.2.html`
|
||||
- `bin/unit_tests/assets/html/pre_code_block.html`
|
||||
- A final segment break immediately before an element end tag is discarded before CSS whitespace
|
||||
collapse/transformation, matching browser behavior for preformatted HTML such as
|
||||
`<pre><code>...\n</code></pre>`.
|
||||
|
||||
Validated:
|
||||
|
||||
- `make -C make/linux -j$(nproc)`
|
||||
- `UIHTML.PreCode*`
|
||||
- `UIHTML.WhiteSpaceCollapse*`
|
||||
- `UIRichText.WhiteSpaceCollapse*`
|
||||
- `RichText.BreakSpaces*`
|
||||
- `UITextNode_EdgeCases.EmptyTextNodesDontAffectLayout`
|
||||
- `UIHTML.WhiteSpaceNowrap*`
|
||||
- `UIHTML.AnchorsSizing`
|
||||
- `UIHTMLTable.complexLayout*` against the original existing golden images
|
||||
- Full Xvfb/ASAN unit suite:
|
||||
`ASAN_OPTIONS=detect_leaks=0 xvfb-run -a -s "-screen 0 1280x1024x24" bin/unit_tests/eepp-unit_tests-debug`
|
||||
ran 478 test cases: 477 passed, 1 skipped (`UIHTML.redditOldThreadWebViewSmoke`).
|
||||
- `git diff --check`
|
||||
- No golden images were refreshed for this feature.
|
||||
|
||||
Pending implementation work:
|
||||
|
||||
- None for the `white-space-collapse` support described by this plan.
|
||||
|
||||
Known follow-up, not part of this recovery:
|
||||
|
||||
- Browser-parity investigation for apparent high pixel-density `em`/font-size differences in code
|
||||
blocks. At pixel density 1, the current `pre_code_block.html` spacing is covered by unit tests.
|
||||
|
||||
## Baseline State Before This Plan
|
||||
|
||||
- `UIRichText` already stores:
|
||||
- `WhiteSpaceCollapse::Collapse`
|
||||
@@ -21,17 +78,17 @@ Reference specs:
|
||||
- `WhiteSpaceCollapse::PreserveBreaks`
|
||||
- `WhiteSpaceCollapse::PreserveSpaces`
|
||||
- `WhiteSpaceCollapse::BreakSpaces`
|
||||
- CSS parsing already recognizes `white-space` and `white-space-collapse`.
|
||||
- CSS parsing already recognized `white-space` and `white-space-collapse`.
|
||||
- `white-space: nowrap` is partially working because it maps to `mLineWrap = false`.
|
||||
- The actual text processing is incomplete:
|
||||
- The actual text processing was incomplete:
|
||||
- `UIRichText::loadFromXmlNode()` collapses all PCDATA through
|
||||
`UIRichText::collapseInternalWhitespace()` before style resolution can know the effective
|
||||
`white-space-collapse` value.
|
||||
- `UIRichText::rebuildRichText()` only distinguishes `Collapse` from all other values.
|
||||
- `preserve-breaks`, `preserve-spaces`, and `break-spaces` do not get their spec behavior.
|
||||
- `break-spaces` has no special line-breaking or intrinsic-size support.
|
||||
- `<pre><code>` is special-cased in `UIRichText::loadFromXmlNode()` into a read-only `UICodeEditor`.
|
||||
That workaround blocks normal HTML styling/layout for code blocks and should become unnecessary.
|
||||
- `<pre><code>` was special-cased in `UIRichText::loadFromXmlNode()` into a read-only `UICodeEditor`.
|
||||
That workaround is now limited to markdown/global opt-in only.
|
||||
|
||||
## Spec Behavior To Support
|
||||
|
||||
@@ -215,20 +272,22 @@ Steps:
|
||||
5. Keep `white-space-trim` out of scope unless it already exists elsewhere; explicitly document this
|
||||
limitation near `applyWhiteSpace()` or in `html-layout-architecture.md`.
|
||||
|
||||
### Phase 6: Replace The `<pre><code>` Fallback
|
||||
### Phase 6: Replace The Generic `<pre><code>` Fallback
|
||||
|
||||
**File:** `src/eepp/ui/uirichtext.cpp`
|
||||
|
||||
Once the whitespace behavior is covered:
|
||||
|
||||
1. Remove the `mTag == "pre" && child.name() == "code"` branch that creates `UICodeEditor`.
|
||||
2. Let `<code>` be created through `UIWidgetCreator::createFromName()` or the existing unknown-element
|
||||
1. Remove the generic `mTag == "pre" && child.name() == "code"` branch that creates `UICodeEditor`.
|
||||
2. Keep the same `UICodeEditor` path only for `UI_TYPE_MARKDOWNVIEW` ancestors and for the explicit
|
||||
global opt-in switch.
|
||||
3. Let ordinary HTML `<code>` be created through `UIWidgetCreator::createFromName()` or the existing
|
||||
fallback to `UITextSpan`.
|
||||
3. Ensure base HTML CSS gives the right defaults:
|
||||
4. Ensure base HTML CSS gives the right defaults:
|
||||
- `pre { white-space: pre; }` or equivalent.
|
||||
- `code { font-family: monospace; }` if not already present.
|
||||
- `pre code` remains normal inline/flow content inside the `<pre>`.
|
||||
4. Preserve `data-language` as ordinary data/style metadata. Do not reintroduce syntax-highlighting
|
||||
5. Preserve `data-language` as ordinary data/style metadata. Do not reintroduce syntax-highlighting
|
||||
behavior through the generic HTML path in this feature.
|
||||
|
||||
### Phase 7: Tests
|
||||
@@ -255,7 +314,7 @@ pixel-only assertions unless testing trailing-space width.
|
||||
Add focused tests in `src/tests/unit_tests/uihtml_tests.cpp`:
|
||||
|
||||
- `WhiteSpaceCollapsePreCodePreservesIndentation`
|
||||
- `WhiteSpaceCollapsePreCodeUsesRichTextNotCodeEditor`
|
||||
- `PreCodeUsesCodeEditorOnlyForMarkdownAncestorOrGlobalOptIn`
|
||||
- `WhiteSpaceCollapsePreLinePreservesBreaksOnly`
|
||||
- `WhiteSpaceCollapseBreakSpacesAffectsIntrinsicWidth`
|
||||
|
||||
@@ -270,7 +329,8 @@ For the `<pre><code>` regression:
|
||||
|
||||
Verify:
|
||||
|
||||
- `code` is not a `UICodeEditor`.
|
||||
- generic HTML `code` is not a `UICodeEditor`.
|
||||
- markdown/global opt-in `code` is a `UICodeEditor`.
|
||||
- indentation before `return` survives.
|
||||
- line count is at least 3.
|
||||
- the `<pre>`/`<code>` content participates in normal RichText layout.
|
||||
@@ -321,7 +381,7 @@ ASAN_OPTIONS=detect_leaks=0 xvfb-run -a -s "-screen 0 1280x1024x24" bin/unit_tes
|
||||
3. Implement all non-`break-spaces` collapse modes.
|
||||
4. Add RichText/LineWrap support for `break-spaces`.
|
||||
5. Add `discard` parser/state behavior.
|
||||
6. Remove the `<pre><code>` `UICodeEditor` fallback.
|
||||
6. Restrict the `<pre><code>` `UICodeEditor` fallback to markdown/global opt-in.
|
||||
7. Update `html-layout-architecture.md` with the final whitespace-processing notes.
|
||||
8. Run focused tests, then `UIHTML.*`, then the full suite.
|
||||
|
||||
@@ -329,7 +389,8 @@ ASAN_OPTIONS=detect_leaks=0 xvfb-run -a -s "-screen 0 1280x1024x24" bin/unit_tes
|
||||
|
||||
- All `white-space-collapse` values have parser, property-string, layout, and test coverage.
|
||||
- Existing `white-space: nowrap` behavior remains passing.
|
||||
- `<pre><code>` renders through normal HTML/RichText nodes, not `UICodeEditor`.
|
||||
- Generic HTML `<pre><code>` renders through normal HTML/RichText nodes, not `UICodeEditor`.
|
||||
- Markdown `<pre><code>` can still render through read-only `UICodeEditor`.
|
||||
- Code indentation and blank lines are preserved in `<pre>` and `white-space: pre` content.
|
||||
- Default HTML whitespace still collapses around block and inline boundaries.
|
||||
- `break-spaces` wraps and measures trailing spaces according to CSS Text Level 4.
|
||||
|
||||
22
bin/unit_tests/assets/html/pre.code.2.html
Normal file
22
bin/unit_tests/assets/html/pre.code.2.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
pre {
|
||||
background-color: #f8f8f8;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-left: 4px solid #c41230;
|
||||
padding: 1em;
|
||||
margin-bottom: 1em;
|
||||
overflow-x: auto;
|
||||
font-size: 0.95em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<pre><code>int x = 42;
|
||||
const char* y = "hello";
|
||||
int z = foo(x, y);
|
||||
</code></pre>
|
||||
</body>
|
||||
</html>
|
||||
6
bin/unit_tests/assets/html/pre.code.html
Normal file
6
bin/unit_tests/assets/html/pre.code.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<html><head></head><body>
|
||||
<pre><code style="background-color: #ccc" class="language-cpp">void main() {
|
||||
printf("Hello World");
|
||||
}
|
||||
</code></pre>
|
||||
</body></html>
|
||||
82
bin/unit_tests/assets/html/pre_code_block.html
Normal file
82
bin/unit_tests/assets/html/pre_code_block.html
Normal file
@@ -0,0 +1,82 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"><head>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--text: #1f2937;
|
||||
--code-bg: #f3f4f6;
|
||||
--code-border: #e5e7eb;
|
||||
}
|
||||
html { font-size: 16px; background: var(--bg); }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.7;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
max-width: 750px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1.5rem 4rem;
|
||||
}
|
||||
code {
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||
font-size: 0.875em;
|
||||
background: var(--code-bg);
|
||||
padding: 0.15em 0.35em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
pre {
|
||||
background: var(--code-bg);
|
||||
border: 1px solid var(--code-border);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
overflow-x: auto;
|
||||
line-height: 1.5;
|
||||
}
|
||||
pre code span {
|
||||
font-weight: normal !important;
|
||||
font-style: normal !important;
|
||||
}
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
div.sourceCode {
|
||||
background: var(--code-bg);
|
||||
border: 1px solid var(--code-border);
|
||||
border-radius: 6px;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
div.sourceCode pre {
|
||||
border: none;
|
||||
margin: 0;
|
||||
}
|
||||
code span.kw { color: #cf222e; }
|
||||
code span.op { color: #cf222e; }
|
||||
code span.dv, code span.fl { color: #0550ae; }
|
||||
code span.co { color: #6e7781; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="sourceCode" id="cb1"><pre class="sourceCode fsharp"><code class="sourceCode fsharp"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> stepper <span class="op">()</span> =</span>
|
||||
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a> //<span class="co"> Execute a single instruction</span></span>
|
||||
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a> //<span class="co"> Each instruction uses a different amount of cycles</span></span>
|
||||
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> mCycles = stepCpu cpu io</span>
|
||||
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a> <span class="kw">for</span> _ <span class="kw">in</span> <span class="fl">1.</span>.mCycles <span class="kw">do</span> </span>
|
||||
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a> stepTimers timer io </span>
|
||||
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a> stepSerial serial io</span>
|
||||
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a> //<span class="co"> The APU technically runs at 4x CPU-cycles, but can be batched</span></span>
|
||||
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a> stepApu apu </span>
|
||||
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> tCycles = mCycles * <span class="dv">4</span> </span>
|
||||
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a> //<span class="co"> The PPU operates at 4x CPU-cycles. The APU should be here too</span></span>
|
||||
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a> <span class="kw">for</span> _ <span class="kw">in</span> <span class="fl">1.</span>.tCycles <span class="kw">do</span> </span>
|
||||
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a> stepPpu ppu </span>
|
||||
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb1-18"><a href="#cb1-18" aria-hidden="true" tabindex="-1"></a> //<span class="co"> Return cycles taken so the frontend runs the emulator at the right speed</span></span>
|
||||
<span id="cb1-19"><a href="#cb1-19" aria-hidden="true" tabindex="-1"></a> mCycles </span></code></pre></div>
|
||||
</body></html>
|
||||
@@ -23,6 +23,8 @@ class EE_API RichText : public Drawable {
|
||||
|
||||
enum class InlineClear { None, Left, Right, Both };
|
||||
|
||||
enum class WhiteSpaceWrapMode { Normal, Preserve, BreakSpaces };
|
||||
|
||||
struct FloatExclusion {
|
||||
Rectf rect;
|
||||
InlineFloat type{ InlineFloat::None };
|
||||
@@ -95,10 +97,22 @@ class EE_API RichText : public Drawable {
|
||||
*/
|
||||
void addSpan( const String& text, const FontStyleConfig& style );
|
||||
|
||||
void addSpan( const String::View& text, const FontStyleConfig& style );
|
||||
|
||||
void addSpan( String&& text, const FontStyleConfig& style );
|
||||
|
||||
void addSpan( const String& text, const FontStyleConfig& style, const Rectf& margin,
|
||||
const Rectf& padding, Float lineHeight = 0,
|
||||
const BaselineAlignValue& baselineAlign = {}, InlineSource source = {} );
|
||||
|
||||
void addSpan( const String::View& text, const FontStyleConfig& style, const Rectf& margin,
|
||||
const Rectf& padding, Float lineHeight = 0,
|
||||
const BaselineAlignValue& baselineAlign = {}, InlineSource source = {} );
|
||||
|
||||
void addSpan( String&& text, const FontStyleConfig& style, const Rectf& margin,
|
||||
const Rectf& padding, Float lineHeight = 0,
|
||||
const BaselineAlignValue& baselineAlign = {}, InlineSource source = {} );
|
||||
|
||||
/**
|
||||
* @brief Adds a text span with individual style parameters.
|
||||
* @param text The text content.
|
||||
@@ -141,6 +155,14 @@ class EE_API RichText : public Drawable {
|
||||
/** @return Whether soft wrapping is enabled. */
|
||||
bool getLineWrap() const { return mLineWrap; }
|
||||
|
||||
void setWhiteSpaceWrapMode( WhiteSpaceWrapMode mode );
|
||||
|
||||
WhiteSpaceWrapMode getWhiteSpaceWrapMode() const { return mWhiteSpaceWrapMode; }
|
||||
|
||||
void setTabWidth( Uint32 tabWidth );
|
||||
|
||||
Uint32 getTabWidth() const { return mTabWidth; }
|
||||
|
||||
bool setExternalFloatExclusions( const std::vector<FloatExclusion>& exclusions );
|
||||
|
||||
const std::vector<FloatExclusion>& getExternalFloatExclusions() const {
|
||||
@@ -402,12 +424,30 @@ class EE_API RichText : public Drawable {
|
||||
const Rectf& padding, Float lineHeight,
|
||||
const BaselineAlignValue& baselineAlign, InlineSource source = {} );
|
||||
|
||||
void addInlineText( const String::View& text, const FontStyleConfig& style, const Rectf& margin,
|
||||
const Rectf& padding, Float lineHeight,
|
||||
const BaselineAlignValue& baselineAlign, InlineSource source = {} );
|
||||
|
||||
void addInlineText( String&& text, const FontStyleConfig& style, const Rectf& margin,
|
||||
const Rectf& padding, Float lineHeight,
|
||||
const BaselineAlignValue& baselineAlign, InlineSource source = {} );
|
||||
|
||||
/** Add an atomic inline-level box to the current inline context. */
|
||||
void addInlineAtomicBox( const Sizef& size, InlineFloat floatType, InlineClear clearType,
|
||||
Float baseline, bool isLineBreak,
|
||||
const BaselineAlignValue& baselineAlign, InlineSource source = {} );
|
||||
|
||||
protected:
|
||||
template <typename TextType>
|
||||
void addSpanImpl( TextType&& text, const FontStyleConfig& style, const Rectf& margin,
|
||||
const Rectf& padding, Float lineHeight,
|
||||
const BaselineAlignValue& baselineAlign, InlineSource source );
|
||||
|
||||
template <typename TextType>
|
||||
void addInlineTextImpl( TextType&& text, const FontStyleConfig& style, const Rectf& margin,
|
||||
const Rectf& padding, Float lineHeight,
|
||||
const BaselineAlignValue& baselineAlign, InlineSource source );
|
||||
|
||||
void rebuildInlineFragments();
|
||||
|
||||
std::vector<InlineItem> mInlineItems;
|
||||
@@ -427,6 +467,8 @@ class EE_API RichText : public Drawable {
|
||||
Float mLineHeight{ 0 };
|
||||
Float mTextIndent{ 0 };
|
||||
bool mLineWrap{ true };
|
||||
WhiteSpaceWrapMode mWhiteSpaceWrapMode{ WhiteSpaceWrapMode::Normal };
|
||||
Uint32 mTabWidth{ 8 };
|
||||
};
|
||||
|
||||
}} // namespace EE::Graphics
|
||||
|
||||
@@ -226,6 +226,7 @@ enum class PropertyId : Uint32 {
|
||||
LineSpacing = String::hash( "line-spacing" ),
|
||||
LineHeight = String::hash( "line-height" ),
|
||||
TextIndent = String::hash( "text-indent" ),
|
||||
TabSize = String::hash( "tab-size" ),
|
||||
GravityOwner = String::hash( "gravity-owner" ),
|
||||
Href = String::hash( "href" ),
|
||||
Focusable = String::hash( "focusable" ),
|
||||
|
||||
@@ -17,10 +17,11 @@ class EE_API UIRichText : public UIHTMLWidget {
|
||||
Preserve,
|
||||
PreserveBreaks,
|
||||
PreserveSpaces,
|
||||
BreakSpaces
|
||||
BreakSpaces,
|
||||
Discard
|
||||
};
|
||||
|
||||
static WhiteSpaceCollapse toWhiteSpaceCollapse( std::string val );
|
||||
static WhiteSpaceCollapse toWhiteSpaceCollapse( std::string_view val );
|
||||
|
||||
static std::string fromWhiteSpaceCollapse( WhiteSpaceCollapse val );
|
||||
|
||||
@@ -31,6 +32,10 @@ class EE_API UIRichText : public UIHTMLWidget {
|
||||
static void rebuildRichText( UILayout* container, RichText& richText,
|
||||
IntrinsicMode mode = IntrinsicMode::None );
|
||||
|
||||
static void setUseCodeEditorForPreCodeBlocks( bool enabled );
|
||||
|
||||
static bool getUseCodeEditorForPreCodeBlocks();
|
||||
|
||||
static UIRichText* New();
|
||||
|
||||
static UIRichText* NewWithTag( const std::string& tag );
|
||||
@@ -140,7 +145,7 @@ class EE_API UIRichText : public UIHTMLWidget {
|
||||
|
||||
void setLineWrap( bool lineWrap );
|
||||
|
||||
void applyWhiteSpace( std::string val );
|
||||
void applyWhiteSpace( std::string_view val );
|
||||
|
||||
Float getLineHeightPx() const;
|
||||
|
||||
@@ -150,6 +155,10 @@ class EE_API UIRichText : public UIHTMLWidget {
|
||||
|
||||
UIRichText* setTextIndentEq( const std::string& eq );
|
||||
|
||||
Uint32 getTabSize() const;
|
||||
|
||||
UIRichText* setTabSize( Uint32 tabSize );
|
||||
|
||||
bool isTextSelectionEnabled() const;
|
||||
|
||||
void setTextSelectionEnabled( bool active );
|
||||
@@ -185,6 +194,7 @@ class EE_API UIRichText : public UIHTMLWidget {
|
||||
mutable bool mLineHeightPxDirty{ true };
|
||||
mutable Float mTextIndentPxCache{ 0 };
|
||||
mutable bool mTextIndentPxDirty{ true };
|
||||
Uint32 mTabSize{ 8 };
|
||||
WhiteSpaceCollapse mWhiteSpaceCollapse{ WhiteSpaceCollapse::Collapse };
|
||||
bool mLineWrap{ true };
|
||||
TextTransform::Value mTextTransform{ TextTransform::None };
|
||||
|
||||
@@ -474,6 +474,17 @@ static void invalidateInlineItemText( std::vector<RichText::InlineItem>& items )
|
||||
}
|
||||
}
|
||||
|
||||
static void setInlineItemTextTabWidth( std::vector<RichText::InlineItem>& items, Uint32 tabWidth ) {
|
||||
for ( auto& item : items ) {
|
||||
if ( item.isTextRun() ) {
|
||||
if ( item.asTextRun().text )
|
||||
item.asTextRun().text->setTabWidth( tabWidth );
|
||||
} else if ( item.isBox() ) {
|
||||
setInlineItemTextTabWidth( item.asBox().children, tabWidth );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct InlineAncestorRef {
|
||||
RichText::RenderSpan::InlinePath path;
|
||||
const RichText::InlineItem::Box* box{ nullptr };
|
||||
@@ -717,7 +728,8 @@ class RichTextInlineLayouter {
|
||||
return false;
|
||||
}
|
||||
|
||||
static Float getMinIntrinsicWidth( const std::vector<RichText::InlineItem>& inlineItems ) {
|
||||
static Float getMinIntrinsicWidth( const std::vector<RichText::InlineItem>& inlineItems,
|
||||
RichText::WhiteSpaceWrapMode whiteSpaceWrapMode ) {
|
||||
Float minW = 0;
|
||||
for ( const auto& run : buildLayoutRuns( inlineItems ) ) {
|
||||
const auto& payload = run.payload;
|
||||
@@ -726,27 +738,39 @@ class RichTextInlineLayouter {
|
||||
if ( !span || span->getString().empty() )
|
||||
continue;
|
||||
const String& s = span->getString();
|
||||
size_t start = 0;
|
||||
size_t end = 0;
|
||||
while ( start < s.size() ) {
|
||||
while ( start < s.size() && ( s[start] == ' ' || s[start] == '\t' ||
|
||||
s[start] == '\n' || s[start] == '\r' ) )
|
||||
start++;
|
||||
end = start;
|
||||
while ( end < s.size() && !( s[end] == ' ' || s[end] == '\t' ||
|
||||
s[end] == '\n' || s[end] == '\r' ) )
|
||||
end++;
|
||||
if ( start < end ) {
|
||||
minW = std::max(
|
||||
minW,
|
||||
Text::getTextWidth( s.substr( start, end - start ),
|
||||
span->getFontStyleConfig() ) +
|
||||
payload.margin.Left + payload.margin.Right + payload.padding.Left +
|
||||
Float spacing = payload.margin.Left + payload.margin.Right + payload.padding.Left +
|
||||
payload.padding.Right +
|
||||
inlineAncestorStartSpacing( inlineItems, payload.inlinePath ) +
|
||||
inlineAncestorEndSpacing( inlineItems, payload.inlinePath ) );
|
||||
inlineAncestorEndSpacing( inlineItems, payload.inlinePath );
|
||||
if ( whiteSpaceWrapMode == RichText::WhiteSpaceWrapMode::BreakSpaces ) {
|
||||
for ( size_t i = 0; i < s.size(); ++i ) {
|
||||
if ( s[i] == '\n' || s[i] == '\r' )
|
||||
continue;
|
||||
minW = std::max(
|
||||
minW, Text::getTextWidth( s.substr( i, 1 ), span->getFontStyleConfig(),
|
||||
span->getTabWidth(), span->getTextHints() ) +
|
||||
spacing );
|
||||
}
|
||||
} else {
|
||||
size_t start = 0;
|
||||
size_t end = 0;
|
||||
while ( start < s.size() ) {
|
||||
while ( start < s.size() && ( s[start] == ' ' || s[start] == '\t' ||
|
||||
s[start] == '\n' || s[start] == '\r' ) )
|
||||
start++;
|
||||
end = start;
|
||||
while ( end < s.size() && !( s[end] == ' ' || s[end] == '\t' ||
|
||||
s[end] == '\n' || s[end] == '\r' ) )
|
||||
end++;
|
||||
if ( start < end ) {
|
||||
minW = std::max( minW, Text::getTextWidth(
|
||||
s.substr( start, end - start ),
|
||||
span->getFontStyleConfig(),
|
||||
span->getTabWidth(), span->getTextHints() ) +
|
||||
spacing );
|
||||
}
|
||||
start = end;
|
||||
}
|
||||
start = end;
|
||||
}
|
||||
} else if ( payload.type == RichText::RenderSpan::Type::Drawable ) {
|
||||
if ( payload.drawable )
|
||||
@@ -777,17 +801,17 @@ class RichTextInlineLayouter {
|
||||
curX += payload.margin.Left + payload.padding.Left +
|
||||
inlineAncestorStartSpacing( inlineItems, payload.inlinePath );
|
||||
while ( ( end = s.find( '\n', start ) ) != String::InvalidPos ) {
|
||||
curX +=
|
||||
Text::getTextWidth( s.substr( start, end - start ),
|
||||
span->getFontStyleConfig(), 4, span->getTextHints() );
|
||||
curX += Text::getTextWidth( s.substr( start, end - start ),
|
||||
span->getFontStyleConfig(), span->getTabWidth(),
|
||||
span->getTextHints() );
|
||||
maxW = std::max(
|
||||
maxW, curX + payload.margin.Right + payload.padding.Right +
|
||||
inlineAncestorEndSpacing( inlineItems, payload.inlinePath ) );
|
||||
curX = 0;
|
||||
start = end + 1;
|
||||
}
|
||||
curX += Text::getTextWidth( s.substr( start ), span->getFontStyleConfig(), 4,
|
||||
span->getTextHints() ) +
|
||||
curX += Text::getTextWidth( s.substr( start ), span->getFontStyleConfig(),
|
||||
span->getTabWidth(), span->getTextHints() ) +
|
||||
payload.margin.Right + payload.padding.Right +
|
||||
inlineAncestorEndSpacing( inlineItems, payload.inlinePath );
|
||||
} else if ( payload.type == RichText::RenderSpan::Type::Drawable ) {
|
||||
@@ -863,7 +887,8 @@ class RichTextInlineLayouter {
|
||||
static LayoutResult layoutNoFloats( const std::vector<RichText::InlineItem>& inlineItems,
|
||||
Float maxLayoutWidth, Float textIndent, Uint32 align,
|
||||
Float forcedLineHeight, const FontStyleConfig& defaultStyle,
|
||||
bool lineWrap ) {
|
||||
bool lineWrap,
|
||||
RichText::WhiteSpaceWrapMode whiteSpaceWrapMode ) {
|
||||
LayoutResult result;
|
||||
result.lines.push_back( RichText::RenderParagraph() );
|
||||
|
||||
@@ -895,14 +920,20 @@ class RichTextInlineLayouter {
|
||||
addInlineSpacingToCurrentLine( result, curX,
|
||||
inlineStartSpacing( payload, inlineItems ) );
|
||||
|
||||
LineWrapInfoEx wrapInfo = computeTextWraps( payload, fontStyle, maxLayoutWidth,
|
||||
lineWrap && maxLayoutWidth > 0, curX );
|
||||
LineWrapInfoEx wrapInfo =
|
||||
computeTextWraps( payload, fontStyle, maxLayoutWidth,
|
||||
lineWrap && maxLayoutWidth > 0, curX, whiteSpaceWrapMode );
|
||||
|
||||
for ( size_t i = 0; i < wrapInfo.wraps.size() - 1; ++i ) {
|
||||
size_t startIdx = wrapInfo.wraps[i];
|
||||
size_t endIdx = wrapInfo.wraps[i + 1];
|
||||
bool isNewline =
|
||||
( endIdx - startIdx == 1 && span->getString()[startIdx] == '\n' );
|
||||
bool startsWithNewline =
|
||||
startIdx < endIdx && span->getString()[startIdx] == '\n';
|
||||
bool isNewline = startsWithNewline && endIdx - startIdx == 1;
|
||||
if ( startsWithNewline && !isNewline ) {
|
||||
curCharIdx++;
|
||||
startIdx++;
|
||||
}
|
||||
|
||||
if ( !isNewline ) {
|
||||
appendTextRenderSpan( result.lines.back(), payload, fontStyle, startIdx,
|
||||
@@ -994,10 +1025,12 @@ class RichTextInlineLayouter {
|
||||
return result;
|
||||
}
|
||||
|
||||
static LayoutResult layoutWithFloats(
|
||||
const std::vector<RichText::InlineItem>& inlineItems, Float maxLayoutWidth,
|
||||
Float textIndent, Uint32 align, Float forcedLineHeight, const FontStyleConfig& defaultStyle,
|
||||
const std::vector<RichText::FloatExclusion>& externalFloatExclusions, bool lineWrap ) {
|
||||
static LayoutResult
|
||||
layoutWithFloats( const std::vector<RichText::InlineItem>& inlineItems, Float maxLayoutWidth,
|
||||
Float textIndent, Uint32 align, Float forcedLineHeight,
|
||||
const FontStyleConfig& defaultStyle,
|
||||
const std::vector<RichText::FloatExclusion>& externalFloatExclusions,
|
||||
bool lineWrap, RichText::WhiteSpaceWrapMode whiteSpaceWrapMode ) {
|
||||
LayoutResult result;
|
||||
result.lines.push_back( RichText::RenderParagraph() );
|
||||
|
||||
@@ -1109,14 +1142,19 @@ class RichTextInlineLayouter {
|
||||
if ( maxLayoutWidth > 0 && maxLayoutWidth < effW )
|
||||
effW = maxLayoutWidth;
|
||||
|
||||
LineWrapInfoEx wrapInfo =
|
||||
computeTextWraps( payload, fontStyle, effW, lineWrap && effW > 0, curX );
|
||||
LineWrapInfoEx wrapInfo = computeTextWraps(
|
||||
payload, fontStyle, effW, lineWrap && effW > 0, curX, whiteSpaceWrapMode );
|
||||
|
||||
for ( size_t i = 0; i < wrapInfo.wraps.size() - 1; ++i ) {
|
||||
size_t startIdx = wrapInfo.wraps[i];
|
||||
size_t endIdx = wrapInfo.wraps[i + 1];
|
||||
bool isNewline =
|
||||
( endIdx - startIdx == 1 && span->getString()[startIdx] == '\n' );
|
||||
bool startsWithNewline =
|
||||
startIdx < endIdx && span->getString()[startIdx] == '\n';
|
||||
bool isNewline = startsWithNewline && endIdx - startIdx == 1;
|
||||
if ( startsWithNewline && !isNewline ) {
|
||||
curCharIdx++;
|
||||
startIdx++;
|
||||
}
|
||||
|
||||
if ( !isNewline ) {
|
||||
appendTextRenderSpan( result.lines.back(), payload, fontStyle, startIdx,
|
||||
@@ -1591,13 +1629,113 @@ class RichTextInlineLayouter {
|
||||
result.lines.back().width += spacing;
|
||||
}
|
||||
|
||||
static bool isBreakSpacesWrapChar( String::StringBaseType ch ) {
|
||||
return ch == ' ' || ch == '\t';
|
||||
}
|
||||
|
||||
static Float breakSpacesTextWidth( const String& string, size_t start, size_t end,
|
||||
const FontStyleConfig& fontStyle, Uint32 tabWidth,
|
||||
Uint32 textHints ) {
|
||||
if ( end <= start )
|
||||
return 0.f;
|
||||
return Text::getTextWidth( string.substr( start, end - start ), fontStyle, tabWidth,
|
||||
textHints );
|
||||
}
|
||||
|
||||
static LineWrapInfoEx computeBreakSpacesTextWraps( const RichText::RenderSpan& span,
|
||||
const FontStyleConfig& fontStyle,
|
||||
Float wrapWidth, bool shouldWrap,
|
||||
Float curX ) {
|
||||
LineWrapInfoEx wrapInfo;
|
||||
const String& string = span.text->getString();
|
||||
wrapInfo.wraps.push_back( 0 );
|
||||
|
||||
if ( string.empty() )
|
||||
return wrapInfo;
|
||||
|
||||
const Uint32 tabWidth = span.text->getTabWidth();
|
||||
const Uint32 textHints = span.text->getTextHints();
|
||||
size_t lineStart = 0;
|
||||
size_t lastBreakAfter = String::InvalidPos;
|
||||
Float lineBaseX = curX;
|
||||
|
||||
auto visualStart = [&string]( size_t start ) {
|
||||
return start < string.size() && string[start] == '\n' ? start + 1 : start;
|
||||
};
|
||||
auto pushWrap = [&wrapInfo]( size_t index, Float width ) {
|
||||
if ( wrapInfo.wraps.back() != static_cast<Int64>( index ) ) {
|
||||
wrapInfo.wrapsWidth.push_back( std::ceil( width ) );
|
||||
wrapInfo.wraps.push_back( index );
|
||||
}
|
||||
};
|
||||
|
||||
for ( size_t idx = 0; idx < string.size(); ++idx ) {
|
||||
if ( string[idx] == '\n' ) {
|
||||
pushWrap( idx,
|
||||
lineBaseX + breakSpacesTextWidth( string, visualStart( lineStart ), idx,
|
||||
fontStyle, tabWidth, textHints ) );
|
||||
lineStart = idx;
|
||||
lastBreakAfter = String::InvalidPos;
|
||||
lineBaseX = 0.f;
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t start = visualStart( lineStart );
|
||||
Float candidateWidth =
|
||||
lineBaseX +
|
||||
breakSpacesTextWidth( string, start, idx + 1, fontStyle, tabWidth, textHints );
|
||||
|
||||
if ( shouldWrap && wrapWidth > 0 && candidateWidth > wrapWidth ) {
|
||||
if ( lastBreakAfter != String::InvalidPos && lastBreakAfter > lineStart ) {
|
||||
pushWrap( lastBreakAfter,
|
||||
lineBaseX + breakSpacesTextWidth( string, start, lastBreakAfter,
|
||||
fontStyle, tabWidth, textHints ) );
|
||||
lineStart = lastBreakAfter;
|
||||
lastBreakAfter = String::InvalidPos;
|
||||
lineBaseX = 0.f;
|
||||
} else if ( isBreakSpacesWrapChar( string[idx] ) ) {
|
||||
pushWrap( idx + 1, candidateWidth );
|
||||
lineStart = idx + 1;
|
||||
lastBreakAfter = String::InvalidPos;
|
||||
lineBaseX = 0.f;
|
||||
continue;
|
||||
} else if ( idx > start ) {
|
||||
pushWrap( idx, lineBaseX + breakSpacesTextWidth( string, start, idx, fontStyle,
|
||||
tabWidth, textHints ) );
|
||||
lineStart = idx;
|
||||
lastBreakAfter = String::InvalidPos;
|
||||
lineBaseX = 0.f;
|
||||
}
|
||||
}
|
||||
|
||||
if ( isBreakSpacesWrapChar( string[idx] ) && lineStart <= idx )
|
||||
lastBreakAfter = idx + 1;
|
||||
}
|
||||
|
||||
if ( !string.empty() && string[string.size() - 1] != '\n' ) {
|
||||
wrapInfo.wrapsWidth.push_back(
|
||||
std::ceil( lineBaseX + breakSpacesTextWidth( string, visualStart( lineStart ),
|
||||
string.size(), fontStyle, tabWidth,
|
||||
span.text->getTextHints() ) ) );
|
||||
}
|
||||
|
||||
if ( wrapInfo.wraps.back() != static_cast<Int64>( string.size() ) )
|
||||
wrapInfo.wraps.push_back( string.size() );
|
||||
|
||||
return wrapInfo;
|
||||
}
|
||||
|
||||
static LineWrapInfoEx computeTextWraps( const RichText::RenderSpan& span,
|
||||
const FontStyleConfig& fontStyle, Float wrapWidth,
|
||||
bool shouldWrap, Float curX ) {
|
||||
bool shouldWrap, Float curX,
|
||||
RichText::WhiteSpaceWrapMode whiteSpaceWrapMode ) {
|
||||
if ( whiteSpaceWrapMode == RichText::WhiteSpaceWrapMode::BreakSpaces )
|
||||
return computeBreakSpacesTextWraps( span, fontStyle, wrapWidth, shouldWrap, curX );
|
||||
|
||||
LineWrapInfoEx wrapInfo = LineWrap::computeLineBreaksEx(
|
||||
span.text->getString(), fontStyle, wrapWidth > 0 ? wrapWidth : 1e9f,
|
||||
shouldWrap ? LineWrapMode::Word : LineWrapMode::NoWrap, false, 4, 0.f,
|
||||
span.text->getTextHints(), false, curX );
|
||||
shouldWrap ? LineWrapMode::Word : LineWrapMode::NoWrap, false, span.text->getTabWidth(),
|
||||
0.f, span.text->getTextHints(), false, curX );
|
||||
|
||||
if ( wrapInfo.wraps.empty() ||
|
||||
wrapInfo.wraps.back() != (Float)span.text->getString().size() )
|
||||
@@ -1656,6 +1794,7 @@ class RichTextInlineLayouter {
|
||||
FontStyleConfig renderStyle = fontStyle;
|
||||
renderStyle.Style |= inlineAncestorTextDecoration( inlineItems, payload.inlinePath );
|
||||
renderSpanText->setStyleConfig( renderStyle );
|
||||
renderSpanText->setTabWidth( payload.text->getTabWidth() );
|
||||
|
||||
Float height = getTextRunLineHeight( payload.text, payload.lineHeight );
|
||||
Float ascent = getTextBaseline( payload.text, payload.lineHeight );
|
||||
@@ -1864,15 +2003,22 @@ class RichTextInlineLayouter {
|
||||
|
||||
} // namespace
|
||||
|
||||
void RichText::addSpan( const String& text, const FontStyleConfig& style, const Rectf& margin,
|
||||
const Rectf& padding, Float lineHeight,
|
||||
const BaselineAlignValue& baselineAlign, InlineSource source ) {
|
||||
template <typename TextType> static void setTextString( Text& text, TextType&& string ) {
|
||||
text.setString( std::forward<TextType>( string ) );
|
||||
}
|
||||
|
||||
template <typename TextType>
|
||||
void RichText::addSpanImpl( TextType&& text, const FontStyleConfig& style, const Rectf& margin,
|
||||
const Rectf& padding, Float lineHeight,
|
||||
const BaselineAlignValue& baselineAlign, InlineSource source ) {
|
||||
if ( text.empty() && margin == Rectf::Zero && padding == Rectf::Zero && lineHeight == 0 )
|
||||
return;
|
||||
|
||||
const bool hasText = !text.empty();
|
||||
auto span = std::make_shared<Text>();
|
||||
span->setString( text );
|
||||
setTextString( *span, std::forward<TextType>( text ) );
|
||||
span->setStyleConfig( style );
|
||||
span->setTabWidth( mTabWidth );
|
||||
|
||||
InlineItem item;
|
||||
InlineItem::Box box;
|
||||
@@ -1882,7 +2028,7 @@ void RichText::addSpan( const String& text, const FontStyleConfig& style, const
|
||||
box.baselineAlign = baselineAlign;
|
||||
box.participatesInLineMetrics = false;
|
||||
box.contributesInlineSpacing = false;
|
||||
if ( !text.empty() || margin != Rectf::Zero || padding != Rectf::Zero || lineHeight > 0 ) {
|
||||
if ( hasText || margin != Rectf::Zero || padding != Rectf::Zero || lineHeight > 0 ) {
|
||||
InlineItem textItem;
|
||||
auto& run = textItem.asTextRun();
|
||||
run.text = span;
|
||||
@@ -1899,6 +2045,24 @@ void RichText::addSpan( const String& text, const FontStyleConfig& style, const
|
||||
invalidateLayout();
|
||||
}
|
||||
|
||||
void RichText::addSpan( const String& text, const FontStyleConfig& style, const Rectf& margin,
|
||||
const Rectf& padding, Float lineHeight,
|
||||
const BaselineAlignValue& baselineAlign, InlineSource source ) {
|
||||
addSpanImpl( text, style, margin, padding, lineHeight, baselineAlign, source );
|
||||
}
|
||||
|
||||
void RichText::addSpan( const String::View& text, const FontStyleConfig& style, const Rectf& margin,
|
||||
const Rectf& padding, Float lineHeight,
|
||||
const BaselineAlignValue& baselineAlign, InlineSource source ) {
|
||||
addSpanImpl( text, style, margin, padding, lineHeight, baselineAlign, source );
|
||||
}
|
||||
|
||||
void RichText::addSpan( String&& text, const FontStyleConfig& style, const Rectf& margin,
|
||||
const Rectf& padding, Float lineHeight,
|
||||
const BaselineAlignValue& baselineAlign, InlineSource source ) {
|
||||
addSpanImpl( std::move( text ), style, margin, padding, lineHeight, baselineAlign, source );
|
||||
}
|
||||
|
||||
void RichText::addDrawable( std::shared_ptr<Drawable> drawable ) {
|
||||
if ( !drawable )
|
||||
return;
|
||||
@@ -1976,17 +2140,19 @@ void RichText::popInlineBox() {
|
||||
mInlinePath.pop_back();
|
||||
}
|
||||
|
||||
void RichText::addInlineText( const String& text, const FontStyleConfig& style, const Rectf& margin,
|
||||
const Rectf& padding, Float lineHeight,
|
||||
const BaselineAlignValue& baselineAlign, InlineSource source ) {
|
||||
template <typename TextType>
|
||||
void RichText::addInlineTextImpl( TextType&& text, const FontStyleConfig& style,
|
||||
const Rectf& margin, const Rectf& padding, Float lineHeight,
|
||||
const BaselineAlignValue& baselineAlign, InlineSource source ) {
|
||||
if ( text.empty() && margin == Rectf::Zero && padding == Rectf::Zero && lineHeight == 0 )
|
||||
return;
|
||||
|
||||
InlineItem textItem;
|
||||
auto& run = textItem.asTextRun();
|
||||
run.text = std::make_shared<Text>();
|
||||
run.text->setString( text );
|
||||
setTextString( *run.text, std::forward<TextType>( text ) );
|
||||
run.text->setStyleConfig( style );
|
||||
run.text->setTabWidth( mTabWidth );
|
||||
run.source = source;
|
||||
run.margin = margin;
|
||||
run.padding = padding;
|
||||
@@ -1998,6 +2164,25 @@ void RichText::addInlineText( const String& text, const FontStyleConfig& style,
|
||||
invalidateLayout();
|
||||
}
|
||||
|
||||
void RichText::addInlineText( const String& text, const FontStyleConfig& style, const Rectf& margin,
|
||||
const Rectf& padding, Float lineHeight,
|
||||
const BaselineAlignValue& baselineAlign, InlineSource source ) {
|
||||
addInlineTextImpl( text, style, margin, padding, lineHeight, baselineAlign, source );
|
||||
}
|
||||
|
||||
void RichText::addInlineText( const String::View& text, const FontStyleConfig& style,
|
||||
const Rectf& margin, const Rectf& padding, Float lineHeight,
|
||||
const BaselineAlignValue& baselineAlign, InlineSource source ) {
|
||||
addInlineTextImpl( text, style, margin, padding, lineHeight, baselineAlign, source );
|
||||
}
|
||||
|
||||
void RichText::addInlineText( String&& text, const FontStyleConfig& style, const Rectf& margin,
|
||||
const Rectf& padding, Float lineHeight,
|
||||
const BaselineAlignValue& baselineAlign, InlineSource source ) {
|
||||
addInlineTextImpl( std::move( text ), style, margin, padding, lineHeight, baselineAlign,
|
||||
source );
|
||||
}
|
||||
|
||||
void RichText::addInlineAtomicBox( const Sizef& size, InlineFloat floatType, InlineClear clearType,
|
||||
Float baseline, bool isLineBreak,
|
||||
const BaselineAlignValue& baselineAlign, InlineSource source ) {
|
||||
@@ -2020,6 +2205,14 @@ void RichText::addSpan( const String& text, const FontStyleConfig& style ) {
|
||||
addSpan( text, style, Rectf::Zero, Rectf::Zero );
|
||||
}
|
||||
|
||||
void RichText::addSpan( const String::View& text, const FontStyleConfig& style ) {
|
||||
addSpan( text, style, Rectf::Zero, Rectf::Zero );
|
||||
}
|
||||
|
||||
void RichText::addSpan( String&& text, const FontStyleConfig& style ) {
|
||||
addSpan( std::move( text ), style, Rectf::Zero, Rectf::Zero );
|
||||
}
|
||||
|
||||
void RichText::addSpan( const String& text, Font* font, Uint32 characterSize, Color color,
|
||||
Uint32 style, Color backgroundColor ) {
|
||||
FontStyleConfig config;
|
||||
@@ -2075,6 +2268,22 @@ void RichText::setLineWrap( bool lineWrap ) {
|
||||
}
|
||||
}
|
||||
|
||||
void RichText::setWhiteSpaceWrapMode( WhiteSpaceWrapMode mode ) {
|
||||
if ( mWhiteSpaceWrapMode != mode ) {
|
||||
mWhiteSpaceWrapMode = mode;
|
||||
invalidateLayout();
|
||||
}
|
||||
}
|
||||
|
||||
void RichText::setTabWidth( Uint32 tabWidth ) {
|
||||
tabWidth = eemax<Uint32>( 1, tabWidth );
|
||||
if ( mTabWidth != tabWidth ) {
|
||||
mTabWidth = tabWidth;
|
||||
setInlineItemTextTabWidth( mInlineItems, mTabWidth );
|
||||
invalidateLayout();
|
||||
}
|
||||
}
|
||||
|
||||
bool RichText::setExternalFloatExclusions( const std::vector<FloatExclusion>& exclusions ) {
|
||||
if ( mExternalFloatExclusions == exclusions )
|
||||
return false;
|
||||
@@ -2090,7 +2299,7 @@ void RichText::invalidate() {
|
||||
}
|
||||
|
||||
Float RichText::getMinIntrinsicWidth() {
|
||||
return RichTextInlineLayouter::getMinIntrinsicWidth( mInlineItems );
|
||||
return RichTextInlineLayouter::getMinIntrinsicWidth( mInlineItems, mWhiteSpaceWrapMode );
|
||||
}
|
||||
|
||||
Float RichText::getMaxIntrinsicWidth() {
|
||||
@@ -2106,8 +2315,9 @@ void RichText::updateLayout() {
|
||||
|
||||
// ─── Inline layouter fast path: no floats or clears ─────────────
|
||||
if ( !hasFloats ) {
|
||||
auto result = RichTextInlineLayouter::layoutNoFloats(
|
||||
mInlineItems, mMaxWidth, mTextIndent, mAlign, mLineHeight, mDefaultStyle, mLineWrap );
|
||||
auto result = RichTextInlineLayouter::layoutNoFloats( mInlineItems, mMaxWidth, mTextIndent,
|
||||
mAlign, mLineHeight, mDefaultStyle,
|
||||
mLineWrap, mWhiteSpaceWrapMode );
|
||||
mLines = std::move( result.lines );
|
||||
mSize = result.size;
|
||||
mTotalCharacterCount = result.totalCharacterCount;
|
||||
@@ -2116,9 +2326,9 @@ void RichText::updateLayout() {
|
||||
return;
|
||||
}
|
||||
|
||||
auto result = RichTextInlineLayouter::layoutWithFloats( mInlineItems, mMaxWidth, mTextIndent,
|
||||
mAlign, mLineHeight, mDefaultStyle,
|
||||
mExternalFloatExclusions, mLineWrap );
|
||||
auto result = RichTextInlineLayouter::layoutWithFloats(
|
||||
mInlineItems, mMaxWidth, mTextIndent, mAlign, mLineHeight, mDefaultStyle,
|
||||
mExternalFloatExclusions, mLineWrap, mWhiteSpaceWrapMode );
|
||||
mLines = std::move( result.lines );
|
||||
mSize = result.size;
|
||||
mTotalCharacterCount = result.totalCharacterCount;
|
||||
|
||||
@@ -234,6 +234,7 @@ void StyleSheetSpecification::registerDefaultProperties() {
|
||||
registerProperty( "line-spacing", "", true ).setType( PropertyType::NumberLength );
|
||||
registerProperty( "line-height", "", true ).setType( PropertyType::NumberLength );
|
||||
registerProperty( "text-indent", "", true ).setType( PropertyType::NumberLength );
|
||||
registerProperty( "tab-size", "8", true ).setType( PropertyType::String );
|
||||
registerProperty( "text-stroke-width", "", true )
|
||||
.setType( PropertyType::NumberLength )
|
||||
.addAlias( "fontoutlinethickness" );
|
||||
|
||||
@@ -23,16 +23,73 @@
|
||||
|
||||
namespace EE { namespace UI {
|
||||
|
||||
UIRichText::WhiteSpaceCollapse UIRichText::toWhiteSpaceCollapse( std::string val ) {
|
||||
String::toLowerInPlace( val );
|
||||
if ( "preserve" == val )
|
||||
struct ProcessedText {
|
||||
String::View view;
|
||||
String storage;
|
||||
bool owns{ false };
|
||||
|
||||
explicit ProcessedText( String::View source ) : view( source ) {}
|
||||
|
||||
bool empty() const { return view.empty(); }
|
||||
|
||||
std::size_t length() const { return view.length(); }
|
||||
|
||||
String::StringBaseType front() const { return view.front(); }
|
||||
|
||||
String::StringBaseType back() const { return view.back(); }
|
||||
|
||||
void setStorage( String&& value ) {
|
||||
storage = std::move( value );
|
||||
view = storage.view();
|
||||
owns = true;
|
||||
}
|
||||
|
||||
void removePrefix( std::size_t count ) {
|
||||
if ( count == 0 || view.empty() )
|
||||
return;
|
||||
if ( owns ) {
|
||||
storage.erase( 0, count );
|
||||
view = storage.view();
|
||||
} else {
|
||||
view.remove_prefix( count );
|
||||
}
|
||||
}
|
||||
|
||||
void removeSuffix( std::size_t count ) {
|
||||
if ( count == 0 || view.empty() )
|
||||
return;
|
||||
if ( owns ) {
|
||||
for ( std::size_t i = 0; i < count && !storage.empty(); ++i )
|
||||
storage.pop_back();
|
||||
view = storage.view();
|
||||
} else {
|
||||
view.remove_suffix( count );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static bool sUseCodeEditorForPreCodeBlocks = false;
|
||||
|
||||
static bool parseTabSizeCount( const std::string& value, Uint32& out ) {
|
||||
Uint32 parsed = 0;
|
||||
if ( !String::fromString( parsed, value ) || parsed == 0 )
|
||||
return false;
|
||||
out = parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
UIRichText::WhiteSpaceCollapse UIRichText::toWhiteSpaceCollapse( std::string_view val ) {
|
||||
val = String::trim( val );
|
||||
if ( String::iequals( val, "preserve" ) )
|
||||
return WhiteSpaceCollapse::Preserve;
|
||||
if ( "preserve-breaks" == val || "preserve-breaks" == val )
|
||||
if ( String::iequals( val, "preserve-breaks" ) )
|
||||
return WhiteSpaceCollapse::PreserveBreaks;
|
||||
if ( "preserve-spaces" == val || "preserve-spaces" == val )
|
||||
if ( String::iequals( val, "preserve-spaces" ) )
|
||||
return WhiteSpaceCollapse::PreserveSpaces;
|
||||
if ( "break-spaces" == val || "break-spaces" == val )
|
||||
if ( String::iequals( val, "break-spaces" ) )
|
||||
return WhiteSpaceCollapse::BreakSpaces;
|
||||
if ( String::iequals( val, "discard" ) )
|
||||
return WhiteSpaceCollapse::Discard;
|
||||
return WhiteSpaceCollapse::Collapse;
|
||||
}
|
||||
|
||||
@@ -46,6 +103,8 @@ std::string UIRichText::fromWhiteSpaceCollapse( WhiteSpaceCollapse val ) {
|
||||
return "preserve-spaces";
|
||||
case WhiteSpaceCollapse::BreakSpaces:
|
||||
return "break-spaces";
|
||||
case WhiteSpaceCollapse::Discard:
|
||||
return "discard";
|
||||
case WhiteSpaceCollapse::Collapse:
|
||||
default:
|
||||
return "collapse";
|
||||
@@ -62,12 +121,30 @@ std::string UIRichText::fromWhiteSpace( WhiteSpaceCollapse collapse, bool lineWr
|
||||
return "break-spaces";
|
||||
case WhiteSpaceCollapse::PreserveSpaces:
|
||||
return lineWrap ? "preserve-spaces" : "preserve nowrap";
|
||||
case WhiteSpaceCollapse::Discard:
|
||||
return lineWrap ? "discard" : "discard nowrap";
|
||||
case WhiteSpaceCollapse::Collapse:
|
||||
default:
|
||||
return lineWrap ? "normal" : "nowrap";
|
||||
}
|
||||
}
|
||||
|
||||
void UIRichText::setUseCodeEditorForPreCodeBlocks( bool enabled ) {
|
||||
sUseCodeEditorForPreCodeBlocks = enabled;
|
||||
}
|
||||
|
||||
bool UIRichText::getUseCodeEditorForPreCodeBlocks() {
|
||||
return sUseCodeEditorForPreCodeBlocks;
|
||||
}
|
||||
|
||||
static bool hasMarkdownViewAncestor( const Node* node ) {
|
||||
for ( const Node* cur = node; cur; cur = cur->getParent() ) {
|
||||
if ( cur->isType( UI_TYPE_MARKDOWNVIEW ) )
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
UIHTMLHtml* UIHTMLHtml::New( const std::string& tag ) {
|
||||
return eeNew( UIHTMLHtml, ( tag ) );
|
||||
}
|
||||
@@ -272,6 +349,7 @@ UIRichText::UIRichText( const std::string& tag ) : UIHTMLWidget( tag ) {
|
||||
}
|
||||
|
||||
mRichText.getFontStyleConfig().FontColor = Color::Black;
|
||||
mRichText.setTabWidth( mTabSize );
|
||||
|
||||
setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent );
|
||||
}
|
||||
@@ -394,6 +472,27 @@ bool UIRichText::applyProperty( const StyleSheetProperty& attribute ) {
|
||||
case PropertyId::TextIndent:
|
||||
setTextIndentEq( attribute.value() );
|
||||
break;
|
||||
case PropertyId::TabSize: {
|
||||
Uint32 tabSize = 0;
|
||||
if ( parseTabSizeCount( attribute.value(), tabSize ) ) {
|
||||
setTabSize( tabSize );
|
||||
} else {
|
||||
Float tabSizePx = lengthFromValue( attribute );
|
||||
const auto& style = mRichText.getFontStyleConfig();
|
||||
if ( tabSizePx > 0 && style.Font != nullptr ) {
|
||||
bool bold = ( style.Style & Text::Style::Bold ) != 0;
|
||||
bool italic = ( style.Style & Text::Style::Italic ) != 0;
|
||||
Float spaceWidth = style.Font
|
||||
->getGlyph( L' ', style.CharacterSize, bold, italic,
|
||||
style.OutlineThickness )
|
||||
.advance;
|
||||
if ( spaceWidth > 0 )
|
||||
setTabSize( eemax<Uint32>(
|
||||
1, static_cast<Uint32>( std::round( tabSizePx / spaceWidth ) ) ) );
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PropertyId::WhiteSpace:
|
||||
applyWhiteSpace( attribute.value() );
|
||||
break;
|
||||
@@ -453,6 +552,8 @@ std::string UIRichText::getPropertyString( const PropertyDefinition* propertyDef
|
||||
return mLineHeightEq.empty() ? "normal" : mLineHeightEq;
|
||||
case PropertyId::TextIndent:
|
||||
return mTextIndentEq.empty() ? "0" : mTextIndentEq;
|
||||
case PropertyId::TabSize:
|
||||
return String::toString( getTabSize() );
|
||||
case PropertyId::WhiteSpace:
|
||||
return fromWhiteSpace( mWhiteSpaceCollapse, mLineWrap );
|
||||
case PropertyId::WhiteSpaceCollapse:
|
||||
@@ -472,7 +573,8 @@ std::vector<PropertyId> UIRichText::getPropertiesImplemented() const {
|
||||
PropertyId::TextStrokeWidth, PropertyId::TextStrokeColor, PropertyId::TextAlign,
|
||||
PropertyId::SelectionColor, PropertyId::SelectionBackColor, PropertyId::TextSelection,
|
||||
PropertyId::TextDecoration, PropertyId::LineHeight, PropertyId::TextIndent,
|
||||
PropertyId::WhiteSpace, PropertyId::WhiteSpaceCollapse, PropertyId::TextTransform };
|
||||
PropertyId::TabSize, PropertyId::WhiteSpace, PropertyId::WhiteSpaceCollapse,
|
||||
PropertyId::TextTransform };
|
||||
props.insert( props.end(), local.begin(), local.end() );
|
||||
return props;
|
||||
}
|
||||
@@ -700,27 +802,70 @@ void UIRichText::setLineWrap( bool lineWrap ) {
|
||||
}
|
||||
}
|
||||
|
||||
void UIRichText::applyWhiteSpace( std::string val ) {
|
||||
String::toLowerInPlace( val );
|
||||
String::trimInPlace( val );
|
||||
if ( val == "normal" ) {
|
||||
void UIRichText::applyWhiteSpace( std::string_view val ) {
|
||||
val = String::trim( val );
|
||||
if ( val.empty() ) {
|
||||
return;
|
||||
}
|
||||
if ( String::iequals( val, "normal" ) ) {
|
||||
setWhiteSpaceCollapse( WhiteSpaceCollapse::Collapse );
|
||||
setLineWrap( true );
|
||||
} else if ( val == "nowrap" ) {
|
||||
} else if ( String::iequals( val, "nowrap" ) ) {
|
||||
setWhiteSpaceCollapse( WhiteSpaceCollapse::Collapse );
|
||||
setLineWrap( false );
|
||||
} else if ( val == "pre" ) {
|
||||
} else if ( String::iequals( val, "pre" ) ) {
|
||||
setWhiteSpaceCollapse( WhiteSpaceCollapse::Preserve );
|
||||
setLineWrap( false );
|
||||
} else if ( val == "pre-wrap" ) {
|
||||
} else if ( String::iequals( val, "pre-wrap" ) ) {
|
||||
setWhiteSpaceCollapse( WhiteSpaceCollapse::Preserve );
|
||||
setLineWrap( true );
|
||||
} else if ( val == "pre-line" ) {
|
||||
} else if ( String::iequals( val, "pre-line" ) ) {
|
||||
setWhiteSpaceCollapse( WhiteSpaceCollapse::PreserveBreaks );
|
||||
setLineWrap( true );
|
||||
} else if ( val == "break-spaces" ) {
|
||||
} else if ( String::iequals( val, "break-spaces" ) ) {
|
||||
setWhiteSpaceCollapse( WhiteSpaceCollapse::BreakSpaces );
|
||||
setLineWrap( true );
|
||||
} else if ( String::iequals( val, "preserve-spaces" ) ) {
|
||||
setWhiteSpaceCollapse( WhiteSpaceCollapse::PreserveSpaces );
|
||||
setLineWrap( true );
|
||||
} else if ( String::iequals( val, "preserve-breaks" ) ) {
|
||||
setWhiteSpaceCollapse( WhiteSpaceCollapse::PreserveBreaks );
|
||||
setLineWrap( true );
|
||||
} else if ( String::iequals( val, "discard" ) ) {
|
||||
setWhiteSpaceCollapse( WhiteSpaceCollapse::Discard );
|
||||
setLineWrap( true );
|
||||
} else {
|
||||
bool foundCollapse = false;
|
||||
bool foundWrap = false;
|
||||
WhiteSpaceCollapse collapse = mWhiteSpaceCollapse;
|
||||
bool lineWrap = mLineWrap;
|
||||
String::readBySeparator(
|
||||
val,
|
||||
[&]( std::string_view token ) {
|
||||
token = String::trim( token );
|
||||
if ( !token.empty() ) {
|
||||
if ( String::iequals( token, "wrap" ) ) {
|
||||
lineWrap = true;
|
||||
foundWrap = true;
|
||||
} else if ( String::iequals( token, "nowrap" ) ) {
|
||||
lineWrap = false;
|
||||
foundWrap = true;
|
||||
} else if ( String::iequals( token, "collapse" ) ||
|
||||
String::iequals( token, "preserve" ) ||
|
||||
String::iequals( token, "preserve-breaks" ) ||
|
||||
String::iequals( token, "preserve-spaces" ) ||
|
||||
String::iequals( token, "break-spaces" ) ||
|
||||
String::iequals( token, "discard" ) ) {
|
||||
collapse = toWhiteSpaceCollapse( token );
|
||||
foundCollapse = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
' ' );
|
||||
if ( foundCollapse )
|
||||
setWhiteSpaceCollapse( collapse );
|
||||
if ( foundWrap )
|
||||
setLineWrap( lineWrap );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -793,6 +938,21 @@ Float UIRichText::getTextIndentPx() const {
|
||||
return mTextIndentPxCache;
|
||||
}
|
||||
|
||||
Uint32 UIRichText::getTabSize() const {
|
||||
return mTabSize;
|
||||
}
|
||||
|
||||
UIRichText* UIRichText::setTabSize( Uint32 tabSize ) {
|
||||
tabSize = eemax<Uint32>( 1, tabSize );
|
||||
if ( mTabSize != tabSize ) {
|
||||
mTabSize = tabSize;
|
||||
mRichText.setTabWidth( mTabSize );
|
||||
notifyLayoutAttrChange();
|
||||
notifyLayoutAttrChangeParent();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
const TextTransform::Value& UIRichText::getTextTransform() const {
|
||||
return mTextTransform;
|
||||
}
|
||||
@@ -812,8 +972,9 @@ void UIRichText::loadFromXmlNode( const pugi::xml_node& node ) {
|
||||
|
||||
for ( pugi::xml_node child = node.first_child(); child; child = child.next_sibling() ) {
|
||||
if ( child.type() == pugi::node_element ) {
|
||||
if ( mTag == "pre" && String::iequals( child.name(), "code" ) ) {
|
||||
// Use a UICodeEditor for <pre><code>
|
||||
if ( mTag == "pre" && String::iequals( child.name(), "code" ) &&
|
||||
( sUseCodeEditorForPreCodeBlocks || hasMarkdownViewAncestor( this ) ) ) {
|
||||
// Use a UICodeEditor for markdown <pre><code> blocks, or when explicitly enabled.
|
||||
UICodeEditor* editor = UICodeEditor::NewWithTag( "code" );
|
||||
if ( editor ) {
|
||||
editor->setParent( this );
|
||||
@@ -835,6 +996,8 @@ void UIRichText::loadFromXmlNode( const pugi::xml_node& node ) {
|
||||
if ( langIt != mDataProperties.end() ) {
|
||||
editor->applyProperty( langIt->second );
|
||||
}
|
||||
editor->getUIStyle()->applyInheritedProperties();
|
||||
editor->onWidgetCreated();
|
||||
}
|
||||
} else if ( String::iequals( child.name(), "style" ) ) {
|
||||
CSS::StyleSheetParser parser;
|
||||
@@ -879,10 +1042,9 @@ void UIRichText::loadFromXmlNode( const pugi::xml_node& node ) {
|
||||
}
|
||||
}
|
||||
} else if ( child.type() == pugi::node_pcdata ) {
|
||||
String collapsed = UIRichText::collapseInternalWhitespace( child.value() );
|
||||
UITextNode* textNode = UITextNode::New();
|
||||
textNode->setParent( this );
|
||||
textNode->setText( collapsed );
|
||||
textNode->setText( child.value() );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -950,6 +1112,294 @@ String UIRichText::UIRichText::collapseInternalWhitespace( const String& s ) {
|
||||
return out;
|
||||
}
|
||||
|
||||
static bool isCSSCollapsibleWhiteSpace( const String::StringBaseType& c ) {
|
||||
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\v' || c == '\f';
|
||||
}
|
||||
|
||||
static bool isCSSSegmentBreak( const String::StringBaseType& c ) {
|
||||
return c == '\n' || c == '\r';
|
||||
}
|
||||
|
||||
static bool isSingleForcedLineBreak( String::View text ) {
|
||||
return text.size() == 1 && text[0] == '\n';
|
||||
}
|
||||
|
||||
static bool shouldCollapseAcrossTextRuns( UIRichText::WhiteSpaceCollapse collapse ) {
|
||||
return collapse == UIRichText::WhiteSpaceCollapse::Collapse ||
|
||||
collapse == UIRichText::WhiteSpaceCollapse::PreserveBreaks;
|
||||
}
|
||||
|
||||
static bool shouldTrimBlockBoundarySpace( UIRichText::WhiteSpaceCollapse collapse ) {
|
||||
return collapse == UIRichText::WhiteSpaceCollapse::Collapse ||
|
||||
collapse == UIRichText::WhiteSpaceCollapse::PreserveBreaks ||
|
||||
collapse == UIRichText::WhiteSpaceCollapse::Discard;
|
||||
}
|
||||
|
||||
static bool trimFinalSegmentBreakBeforeEndTag( ProcessedText& text ) {
|
||||
if ( text.empty() )
|
||||
return false;
|
||||
|
||||
if ( text.back() == '\n' ) {
|
||||
std::size_t count = 1;
|
||||
if ( text.length() > 1 && text.view[text.length() - 2] == '\r' )
|
||||
count++;
|
||||
text.removeSuffix( count );
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( text.back() == '\r' ) {
|
||||
text.removeSuffix( 1 );
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool containsCarriageReturn( String::View text ) {
|
||||
for ( auto c : text ) {
|
||||
if ( c == '\r' )
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool preserveSpacesNeedsTransform( String::View text ) {
|
||||
for ( auto c : text ) {
|
||||
if ( isCSSCollapsibleWhiteSpace( c ) && c != ' ' )
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool preserveBreaksNeedsTransform( String::View text ) {
|
||||
bool inSpace = false;
|
||||
for ( std::size_t i = 0; i < text.size(); ++i ) {
|
||||
auto c = text[i];
|
||||
if ( isCSSSegmentBreak( c ) ) {
|
||||
if ( c == '\r' )
|
||||
return true;
|
||||
inSpace = false;
|
||||
} else if ( isCSSCollapsibleWhiteSpace( c ) ) {
|
||||
if ( c != ' ' || inSpace )
|
||||
return true;
|
||||
inSpace = true;
|
||||
} else {
|
||||
inSpace = false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool collapseNeedsTransform( String::View text ) {
|
||||
bool inSpace = false;
|
||||
for ( auto c : text ) {
|
||||
if ( isCSSCollapsibleWhiteSpace( c ) ) {
|
||||
if ( c != ' ' || inSpace )
|
||||
return true;
|
||||
inSpace = true;
|
||||
} else {
|
||||
inSpace = false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool discardNeedsTransform( String::View text ) {
|
||||
for ( auto c : text ) {
|
||||
if ( isCSSCollapsibleWhiteSpace( c ) )
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static ProcessedText processWhiteSpaceForLayout( String::View source,
|
||||
UIRichText::WhiteSpaceCollapse collapse,
|
||||
bool prevIsInline, bool nextIsInline,
|
||||
bool lastSpanEndsWithSpace,
|
||||
bool discardSegmentBreakBeforeEndTag ) {
|
||||
ProcessedText text( source );
|
||||
if ( text.empty() )
|
||||
return text;
|
||||
|
||||
if ( discardSegmentBreakBeforeEndTag )
|
||||
trimFinalSegmentBreakBeforeEndTag( text );
|
||||
|
||||
if ( text.empty() )
|
||||
return text;
|
||||
|
||||
String out;
|
||||
bool needsStorage = false;
|
||||
|
||||
switch ( collapse ) {
|
||||
case UIRichText::WhiteSpaceCollapse::Preserve:
|
||||
case UIRichText::WhiteSpaceCollapse::BreakSpaces:
|
||||
needsStorage = containsCarriageReturn( text.view );
|
||||
if ( needsStorage ) {
|
||||
out.reserve( text.length() );
|
||||
for ( std::size_t i = 0; i < text.length(); ++i ) {
|
||||
if ( text.view[i] == '\r' ) {
|
||||
out += '\n';
|
||||
if ( i + 1 < text.length() && text.view[i + 1] == '\n' )
|
||||
i++;
|
||||
} else {
|
||||
out += text.view[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case UIRichText::WhiteSpaceCollapse::PreserveSpaces:
|
||||
needsStorage = preserveSpacesNeedsTransform( text.view );
|
||||
if ( needsStorage ) {
|
||||
out.reserve( text.length() );
|
||||
for ( std::size_t i = 0; i < text.length(); ++i ) {
|
||||
if ( isCSSSegmentBreak( text.view[i] ) ) {
|
||||
out += ' ';
|
||||
if ( text.view[i] == '\r' && i + 1 < text.length() &&
|
||||
text.view[i + 1] == '\n' )
|
||||
i++;
|
||||
} else {
|
||||
out += isCSSCollapsibleWhiteSpace( text.view[i] ) ? ' ' : text.view[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case UIRichText::WhiteSpaceCollapse::PreserveBreaks:
|
||||
needsStorage = preserveBreaksNeedsTransform( text.view );
|
||||
if ( needsStorage ) {
|
||||
out.reserve( text.length() );
|
||||
bool inSpace = false;
|
||||
for ( std::size_t i = 0; i < text.length(); ++i ) {
|
||||
if ( isCSSSegmentBreak( text.view[i] ) ) {
|
||||
out += '\n';
|
||||
inSpace = false;
|
||||
if ( text.view[i] == '\r' && i + 1 < text.length() &&
|
||||
text.view[i + 1] == '\n' )
|
||||
i++;
|
||||
} else if ( isCSSCollapsibleWhiteSpace( text.view[i] ) ) {
|
||||
if ( !inSpace ) {
|
||||
out += ' ';
|
||||
inSpace = true;
|
||||
}
|
||||
} else {
|
||||
out += text.view[i];
|
||||
inSpace = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case UIRichText::WhiteSpaceCollapse::Discard:
|
||||
needsStorage = discardNeedsTransform( text.view );
|
||||
if ( needsStorage ) {
|
||||
out.reserve( text.length() );
|
||||
for ( auto c : text.view ) {
|
||||
if ( !isCSSCollapsibleWhiteSpace( c ) )
|
||||
out += c;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case UIRichText::WhiteSpaceCollapse::Collapse:
|
||||
default:
|
||||
needsStorage = collapseNeedsTransform( text.view );
|
||||
if ( needsStorage ) {
|
||||
out.reserve( text.length() );
|
||||
bool inSpace = false;
|
||||
for ( auto c : text.view ) {
|
||||
if ( isCSSCollapsibleWhiteSpace( c ) ) {
|
||||
if ( !inSpace ) {
|
||||
out += ' ';
|
||||
inSpace = true;
|
||||
}
|
||||
} else {
|
||||
out += c;
|
||||
inSpace = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if ( needsStorage )
|
||||
text.setStorage( std::move( out ) );
|
||||
|
||||
if ( shouldTrimBlockBoundarySpace( collapse ) ) {
|
||||
if ( !prevIsInline && !text.empty() && text.front() == ' ' )
|
||||
text.removePrefix( 1 );
|
||||
if ( !nextIsInline && !text.empty() && text.back() == ' ' )
|
||||
text.removeSuffix( 1 );
|
||||
if ( lastSpanEndsWithSpace && !text.empty() && text.front() == ' ' )
|
||||
text.removePrefix( 1 );
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
static void applyTextTransformValue( String& text, TextTransform::Value transform ) {
|
||||
switch ( transform ) {
|
||||
case TextTransform::LowerCase:
|
||||
text = text.toLower();
|
||||
break;
|
||||
case TextTransform::UpperCase:
|
||||
text = text.toUpper();
|
||||
break;
|
||||
case TextTransform::Capitalize:
|
||||
text = text.capitalize();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static String materializeTransformedText( String::View text, TextTransform::Value transform ) {
|
||||
String transformed( text );
|
||||
if ( transform != TextTransform::None )
|
||||
applyTextTransformValue( transformed, transform );
|
||||
return transformed;
|
||||
}
|
||||
|
||||
static void addProcessedTextSpan( RichText& richText, ProcessedText&& text,
|
||||
const FontStyleConfig& style, bool inlineText,
|
||||
RichText::InlineSource source = {} ) {
|
||||
if ( text.owns ) {
|
||||
if ( inlineText )
|
||||
richText.addInlineText( std::move( text.storage ), style, Rectf::Zero, Rectf::Zero, 0,
|
||||
{}, source );
|
||||
else
|
||||
richText.addSpan( std::move( text.storage ), style, Rectf::Zero, Rectf::Zero, 0, {},
|
||||
source );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( inlineText )
|
||||
richText.addInlineText( text.view, style, Rectf::Zero, Rectf::Zero, 0, {}, source );
|
||||
else
|
||||
richText.addSpan( text.view, style, Rectf::Zero, Rectf::Zero, 0, {}, source );
|
||||
}
|
||||
|
||||
static UIRichText::WhiteSpaceCollapse getEffectiveWhiteSpaceCollapse( Node* node,
|
||||
UIRichText* fallback ) {
|
||||
for ( Node* cur = node; cur; cur = cur->getParent() ) {
|
||||
if ( cur->isType( UI_TYPE_TEXTSPAN ) || cur->isType( UI_TYPE_RICHTEXT ) )
|
||||
return cur->asType<UIRichText>()->getWhiteSpaceCollapse();
|
||||
}
|
||||
return fallback ? fallback->getWhiteSpaceCollapse() : UIRichText::WhiteSpaceCollapse::Collapse;
|
||||
}
|
||||
|
||||
static RichText::WhiteSpaceWrapMode
|
||||
toRichTextWhiteSpaceWrapMode( UIRichText::WhiteSpaceCollapse collapse ) {
|
||||
switch ( collapse ) {
|
||||
case UIRichText::WhiteSpaceCollapse::BreakSpaces:
|
||||
return RichText::WhiteSpaceWrapMode::BreakSpaces;
|
||||
case UIRichText::WhiteSpaceCollapse::Preserve:
|
||||
case UIRichText::WhiteSpaceCollapse::PreserveSpaces:
|
||||
return RichText::WhiteSpaceWrapMode::Preserve;
|
||||
case UIRichText::WhiteSpaceCollapse::Collapse:
|
||||
case UIRichText::WhiteSpaceCollapse::PreserveBreaks:
|
||||
case UIRichText::WhiteSpaceCollapse::Discard:
|
||||
default:
|
||||
return RichText::WhiteSpaceWrapMode::Normal;
|
||||
}
|
||||
}
|
||||
|
||||
static Float getAtomicInlineBoxBaseline( UIWidget* widget, const Sizef& widgetSize,
|
||||
const Rectf& margin ) {
|
||||
Float fallbackBaseline = widgetSize.getHeight() + margin.Top + margin.Bottom;
|
||||
@@ -1115,16 +1565,19 @@ static Drawable* getInlineBorderDrawable( UIWidget* widget ) {
|
||||
|
||||
void UIRichText::rebuildRichText( UILayout* container, RichText& richText, IntrinsicMode mode ) {
|
||||
richText.clear();
|
||||
if ( container->isType( UI_TYPE_RICHTEXT ) ) {
|
||||
if ( container->isType( UI_TYPE_RICHTEXT ) || container->isType( UI_TYPE_TEXTSPAN ) ) {
|
||||
auto* uiRt = static_cast<UIRichText*>( container );
|
||||
richText.setLineHeight( uiRt->getLineHeightPx() );
|
||||
richText.setTextIndent( uiRt->getTextIndentPx() );
|
||||
richText.setLineWrap( uiRt->getLineWrap() );
|
||||
richText.setTabWidth( uiRt->getTabSize() );
|
||||
richText.setWhiteSpaceWrapMode(
|
||||
toRichTextWhiteSpaceWrapMode( uiRt->getWhiteSpaceCollapse() ) );
|
||||
}
|
||||
bool shouldCollapse = container->isType( UI_TYPE_RICHTEXT )
|
||||
? static_cast<UIRichText*>( container )->getWhiteSpaceCollapse() ==
|
||||
WhiteSpaceCollapse::Collapse
|
||||
: true;
|
||||
UIRichText* containerRichText =
|
||||
container->isType( UI_TYPE_RICHTEXT ) || container->isType( UI_TYPE_TEXTSPAN )
|
||||
? container->asType<UIRichText>()
|
||||
: nullptr;
|
||||
bool lastSpanEndsWithSpace = false;
|
||||
Float maxWidth = 0;
|
||||
bool isInlineBlockTextSpan =
|
||||
@@ -1176,22 +1629,6 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
|
||||
return TextTransform::None;
|
||||
};
|
||||
|
||||
auto applyTextTransform = []( String& text, TextTransform::Value tt ) {
|
||||
switch ( tt ) {
|
||||
case TextTransform::LowerCase:
|
||||
text = text.toLower();
|
||||
break;
|
||||
case TextTransform::UpperCase:
|
||||
text = text.toUpper();
|
||||
break;
|
||||
case TextTransform::Capitalize:
|
||||
text = text.capitalize();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if ( container->isType( UI_TYPE_TEXTSPAN ) ) {
|
||||
UITextSpan* selfSpan = container->asType<UITextSpan>();
|
||||
Node* parentNode = container->getParent();
|
||||
@@ -1199,19 +1636,23 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
|
||||
parentNode->asType<UIHTMLWidget>()->isFlex();
|
||||
if ( !selfSpan->getText().empty() && ( !selfSpan->isInline() || parentIsFlex ) &&
|
||||
NULL != selfSpan->getFontStyleConfig().Font ) {
|
||||
String::View selfText = selfSpan->getText().view();
|
||||
String transformed;
|
||||
auto collapse = getEffectiveWhiteSpaceCollapse( selfSpan, containerRichText );
|
||||
richText.setWhiteSpaceWrapMode( toRichTextWhiteSpaceWrapMode( collapse ) );
|
||||
auto selfText = processWhiteSpaceForLayout( selfSpan->getText().view(), collapse, true,
|
||||
true, lastSpanEndsWithSpace,
|
||||
selfSpan->getFirstChild() == nullptr );
|
||||
auto tt = getEffectiveTextTransform( selfSpan );
|
||||
if ( tt != TextTransform::None ) {
|
||||
transformed = selfText;
|
||||
applyTextTransform( transformed, tt );
|
||||
selfText = transformed.view();
|
||||
}
|
||||
FontStyleConfig style = selfSpan->getFontStyleConfig();
|
||||
style.BackgroundColor = Color::Transparent;
|
||||
richText.addSpan( selfText, style, Rectf::Zero, Rectf::Zero, 0, {} );
|
||||
if ( shouldCollapse )
|
||||
lastSpanEndsWithSpace = !selfText.empty() && selfText.back() == ' ';
|
||||
bool endsWithSpace = shouldCollapseAcrossTextRuns( collapse ) && !selfText.empty() &&
|
||||
selfText.back() == ' ';
|
||||
if ( tt != TextTransform::None )
|
||||
richText.addSpan( materializeTransformedText( selfText.view, tt ), style,
|
||||
Rectf::Zero, Rectf::Zero, 0, {} );
|
||||
else
|
||||
addProcessedTextSpan( richText, std::move( selfText ), style, false );
|
||||
if ( shouldCollapseAcrossTextRuns( collapse ) )
|
||||
lastSpanEndsWithSpace = endsWithSpace;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1255,7 +1696,6 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
|
||||
|
||||
if ( node->isTextNode() ) {
|
||||
UITextNode* textNode = node->asType<UITextNode>();
|
||||
String text = textNode->getText();
|
||||
|
||||
Node* prev = findLogicalPrev( node );
|
||||
bool prevIsInline =
|
||||
@@ -1271,28 +1711,33 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
|
||||
return htmlWidget->getCSSFloat() != CSSFloat::None || htmlWidget->isOutOfFlow();
|
||||
};
|
||||
|
||||
if ( shouldCollapse && textNode->isWhitespaceOnly() &&
|
||||
auto collapse =
|
||||
getEffectiveWhiteSpaceCollapse( textNode->getParent(), containerRichText );
|
||||
richText.setWhiteSpaceWrapMode( toRichTextWhiteSpaceWrapMode( collapse ) );
|
||||
|
||||
if ( collapse == WhiteSpaceCollapse::Collapse && textNode->isWhitespaceOnly() &&
|
||||
( !prevIsInline || isFloatingOrOutOfFlow( prev ) ) &&
|
||||
( !nextIsInline || isFloatingOrOutOfFlow( next ) ) ) {
|
||||
textNode->setLayoutCharCount( 0 );
|
||||
return;
|
||||
}
|
||||
|
||||
// Strip leading space if prev is not inline (block boundary)
|
||||
if ( !prevIsInline && !text.empty() && text[0] == ' ' )
|
||||
text = text.substr( 1 );
|
||||
auto text = processWhiteSpaceForLayout(
|
||||
textNode->getText().view(), collapse, prevIsInline, nextIsInline,
|
||||
lastSpanEndsWithSpace, textNode->getNextNode() == nullptr );
|
||||
auto tt = getEffectiveTextTransform( textNode );
|
||||
String transformed;
|
||||
if ( tt != TextTransform::None ) {
|
||||
transformed = materializeTransformedText( text.view, tt );
|
||||
text.view = transformed.view();
|
||||
text.owns = false;
|
||||
}
|
||||
|
||||
// Strip trailing space if next is not inline
|
||||
if ( !nextIsInline && !text.empty() && text.back() == ' ' )
|
||||
text = text.substr( 0, text.size() - 1 );
|
||||
|
||||
if ( shouldCollapse && lastSpanEndsWithSpace && !text.empty() && text[0] == ' ' )
|
||||
text = text.substr( 1 );
|
||||
|
||||
{
|
||||
auto tt = getEffectiveTextTransform( textNode );
|
||||
if ( tt != TextTransform::None )
|
||||
applyTextTransform( text, tt );
|
||||
if ( isSingleForcedLineBreak( text.view ) ) {
|
||||
textNode->setLayoutCharCount( text.length() );
|
||||
richText.addLineBreak();
|
||||
lastSpanEndsWithSpace = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if ( text.empty() ) {
|
||||
@@ -1302,7 +1747,7 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
|
||||
|
||||
textNode->setLayoutCharCount( text.length() );
|
||||
|
||||
if ( shouldCollapse )
|
||||
if ( shouldCollapseAcrossTextRuns( collapse ) )
|
||||
lastSpanEndsWithSpace = text.back() == ' ';
|
||||
|
||||
FontStyleConfig style;
|
||||
@@ -1313,12 +1758,21 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
|
||||
} else {
|
||||
style = richText.getFontStyleConfig();
|
||||
}
|
||||
if ( inlineBoxDepth > 0 )
|
||||
richText.addInlineText( text, style, Rectf::Zero, Rectf::Zero, 0, {},
|
||||
toRichTextTextSource( textNode ) );
|
||||
else
|
||||
richText.addSpan( text, style, Rectf::Zero, Rectf::Zero, 0, {},
|
||||
toRichTextTextSource( textNode ) );
|
||||
if ( inlineBoxDepth > 0 ) {
|
||||
if ( tt != TextTransform::None )
|
||||
richText.addInlineText( std::move( transformed ), style, Rectf::Zero,
|
||||
Rectf::Zero, 0, {}, toRichTextTextSource( textNode ) );
|
||||
else
|
||||
addProcessedTextSpan( richText, std::move( text ), style, true,
|
||||
toRichTextTextSource( textNode ) );
|
||||
} else {
|
||||
if ( tt != TextTransform::None )
|
||||
richText.addSpan( std::move( transformed ), style, Rectf::Zero, Rectf::Zero, 0,
|
||||
{}, toRichTextTextSource( textNode ) );
|
||||
else
|
||||
addProcessedTextSpan( richText, std::move( text ), style, false,
|
||||
toRichTextTextSource( textNode ) );
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1372,8 +1826,6 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
|
||||
inlineBoxDepth++;
|
||||
|
||||
if ( hasOwnText ) {
|
||||
String::View spanText = span->getText().view();
|
||||
|
||||
Node* prev = findLogicalPrev( node );
|
||||
bool prevIsInline =
|
||||
prev && prev->isWidget() && prev->asType<UIWidget>()->isInlineDisplay();
|
||||
@@ -1382,29 +1834,26 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
|
||||
bool nextIsInline =
|
||||
next && next->isWidget() && next->asType<UIWidget>()->isInlineDisplay();
|
||||
|
||||
if ( !prevIsInline && !spanText.empty() && spanText[0] == ' ' )
|
||||
spanText = spanText.substr( 1 );
|
||||
if ( !nextIsInline && !spanText.empty() && spanText.back() == ' ' )
|
||||
spanText = spanText.substr( 0, spanText.size() - 1 );
|
||||
|
||||
if ( shouldCollapse && lastSpanEndsWithSpace && !spanText.empty() &&
|
||||
spanText[0] == ' ' )
|
||||
spanText = spanText.substr( 1 );
|
||||
|
||||
String transformed;
|
||||
auto collapse = getEffectiveWhiteSpaceCollapse( span, containerRichText );
|
||||
richText.setWhiteSpaceWrapMode( toRichTextWhiteSpaceWrapMode( collapse ) );
|
||||
auto spanText = processWhiteSpaceForLayout(
|
||||
span->getText().view(), collapse, prevIsInline, nextIsInline,
|
||||
lastSpanEndsWithSpace, span->getFirstChild() == nullptr );
|
||||
auto tt = getEffectiveTextTransform( span );
|
||||
if ( tt != TextTransform::None ) {
|
||||
transformed = spanText;
|
||||
applyTextTransform( transformed, tt );
|
||||
spanText = transformed.view();
|
||||
String transformed = materializeTransformedText( spanText.view, tt );
|
||||
spanText.setStorage( std::move( transformed ) );
|
||||
}
|
||||
|
||||
if ( !spanText.empty() ) {
|
||||
richText.addInlineText( spanText, span->getFontStyleConfig(), Rectf::Zero,
|
||||
Rectf::Zero, 0, {} );
|
||||
span->setLayoutCharCount( spanText.length() );
|
||||
if ( shouldCollapse )
|
||||
lastSpanEndsWithSpace = spanText.back() == ' ';
|
||||
auto spanTextLength = spanText.length();
|
||||
bool endsWithSpace = shouldCollapseAcrossTextRuns( collapse ) &&
|
||||
!spanText.empty() && spanText.back() == ' ';
|
||||
addProcessedTextSpan( richText, std::move( spanText ),
|
||||
span->getFontStyleConfig(), true );
|
||||
span->setLayoutCharCount( spanTextLength );
|
||||
if ( shouldCollapseAcrossTextRuns( collapse ) )
|
||||
lastSpanEndsWithSpace = endsWithSpace;
|
||||
} else {
|
||||
span->setLayoutCharCount( 0 );
|
||||
}
|
||||
@@ -1419,7 +1868,9 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
|
||||
spanChild = spanChild->getNextNode();
|
||||
}
|
||||
|
||||
if ( shouldCollapse && span->isInlineBlock() )
|
||||
if ( shouldCollapseAcrossTextRuns(
|
||||
getEffectiveWhiteSpaceCollapse( span, containerRichText ) ) &&
|
||||
span->isInlineBlock() )
|
||||
lastSpanEndsWithSpace = true;
|
||||
|
||||
inlineBoxDepth--;
|
||||
|
||||
@@ -75,9 +75,8 @@ void UITextSpan::draw() {
|
||||
// flex container (blockification per CSS Flexbox §4). In that case the
|
||||
// parent flex container does NOT render its text via rebuildRichText(),
|
||||
// so the span must draw itself.
|
||||
if ( !isInline() ||
|
||||
( getParent() && getParent()->isType( UI_TYPE_HTML_WIDGET ) &&
|
||||
getParent()->asType<UIHTMLWidget>()->isFlex() ) ) {
|
||||
if ( !isInline() || ( getParent() && getParent()->isType( UI_TYPE_HTML_WIDGET ) &&
|
||||
getParent()->asType<UIHTMLWidget>()->isFlex() ) ) {
|
||||
UIRichText::draw();
|
||||
}
|
||||
}
|
||||
@@ -486,16 +485,15 @@ void UITextSpan::loadFromXmlNode( const pugi::xml_node& node ) {
|
||||
widget->loadFromXmlNode( child );
|
||||
}
|
||||
} else if ( child.type() == pugi::node_pcdata ) {
|
||||
String collapsed = UIRichText::collapseInternalWhitespace( child.value() );
|
||||
UITextNode* textNode = UITextNode::New();
|
||||
textNode->setParent( this );
|
||||
textNode->setText( collapsed );
|
||||
textNode->setText( child.value() );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for ( pugi::xml_node child = node.first_child(); child; child = child.next_sibling() ) {
|
||||
if ( child.type() == pugi::node_pcdata ) {
|
||||
mText += UIRichText::collapseInternalWhitespace( child.value() );
|
||||
mText += child.value();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ h5 { font-size: 0.83em; font-weight: bold; margin: 1.67em 0; }
|
||||
h6 { font-size: 0.67em; font-weight: bold; margin: 2.33em 0; }
|
||||
|
||||
p { margin: 1em 0; }
|
||||
pre { margin: 1em 0; }
|
||||
pre { margin: 1em 0; white-space: pre; }
|
||||
blockquote { margin: 1em 0; }
|
||||
hr { margin: 0.5em 0; background-color: gray; height: 1dp; }
|
||||
ul, ol, dl { margin: 1em 0; }
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <eepp/graphics/fonttruetype.hpp>
|
||||
#include <eepp/graphics/primitives.hpp>
|
||||
#include <eepp/graphics/richtext.hpp>
|
||||
#include <eepp/graphics/text.hpp>
|
||||
#include <eepp/scene/scenemanager.hpp>
|
||||
#include <eepp/system/filesystem.hpp>
|
||||
#include <eepp/system/scopedop.hpp>
|
||||
@@ -56,6 +57,36 @@ static void destroyRichTextScene( UI::UISceneNode* sceneNode ) {
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
static String richTextRenderedText( const RichText& richText ) {
|
||||
String text;
|
||||
const auto& lines = richText.getLines();
|
||||
for ( size_t lineIndex = 0; lineIndex < lines.size(); ++lineIndex ) {
|
||||
if ( lineIndex > 0 )
|
||||
text += '\n';
|
||||
for ( const auto& span : lines[lineIndex].spans ) {
|
||||
if ( span.type == RichText::RenderSpan::Type::Text && span.text )
|
||||
text += span.text->getString();
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
static String richTextLineText( const RichText& richText, size_t lineIndex ) {
|
||||
String text;
|
||||
const auto& lines = richText.getLines();
|
||||
if ( lineIndex >= lines.size() )
|
||||
return text;
|
||||
for ( const auto& span : lines[lineIndex].spans ) {
|
||||
if ( span.type == RichText::RenderSpan::Type::Text && span.text )
|
||||
text += span.text->getString();
|
||||
}
|
||||
while ( !text.empty() && ( text.front() == '\n' || text.front() == '\r' ) )
|
||||
text = text.substr( 1 );
|
||||
while ( !text.empty() && ( text.back() == '\n' || text.back() == '\r' ) )
|
||||
text.pop_back();
|
||||
return text;
|
||||
}
|
||||
|
||||
UTEST( RichText, basicFunctionality ) {
|
||||
Engine::instance()->createWindow( WindowSettings( 800, 600, "RichText Test",
|
||||
WindowStyle::Default, WindowBackend::Default,
|
||||
@@ -1576,6 +1607,183 @@ UTEST( UIRichText, WhitespaceCollapseCodeTest ) {
|
||||
destroyRichTextScene( sceneNode );
|
||||
}
|
||||
|
||||
UTEST( UIRichText, WhiteSpaceCollapsePreserveSpacesAndBreaks ) {
|
||||
auto sceneNode = createRichTextScene();
|
||||
ASSERT_TRUE( sceneNode != nullptr );
|
||||
|
||||
String xml = R"xml(
|
||||
<RichText id="rt" white-space-collapse="preserve" white-space="pre-wrap">alpha beta
|
||||
gamma</RichText>
|
||||
)xml";
|
||||
|
||||
sceneNode->loadLayoutFromString( xml );
|
||||
UI::UIRichText* rt = sceneNode->find<UI::UIRichText>( "rt" );
|
||||
ASSERT_TRUE( rt != nullptr );
|
||||
|
||||
sceneNode->update( Time::Zero );
|
||||
|
||||
const auto& richText = *rt->getRichTextPtr();
|
||||
EXPECT_EQ( richText.getLines().size(), (size_t)2 );
|
||||
EXPECT_STRINGEQ( richTextLineText( richText, 0 ), "alpha beta" );
|
||||
EXPECT_STRINGEQ( richTextLineText( richText, 1 ), " gamma" );
|
||||
|
||||
destroyRichTextScene( sceneNode );
|
||||
}
|
||||
|
||||
UTEST( UIRichText, WhiteSpaceCollapsePreserveBreaks ) {
|
||||
auto sceneNode = createRichTextScene();
|
||||
ASSERT_TRUE( sceneNode != nullptr );
|
||||
|
||||
String xml = R"xml(
|
||||
<RichText id="rt" white-space-collapse="preserve-breaks">alpha beta
|
||||
gamma</RichText>
|
||||
)xml";
|
||||
|
||||
sceneNode->loadLayoutFromString( xml );
|
||||
UI::UIRichText* rt = sceneNode->find<UI::UIRichText>( "rt" );
|
||||
ASSERT_TRUE( rt != nullptr );
|
||||
|
||||
sceneNode->update( Time::Zero );
|
||||
|
||||
const auto& richText = *rt->getRichTextPtr();
|
||||
EXPECT_EQ( richText.getLines().size(), (size_t)2 );
|
||||
EXPECT_STRINGEQ( richTextLineText( richText, 0 ), "alpha beta" );
|
||||
EXPECT_STRINGEQ( richTextLineText( richText, 1 ), " gamma" );
|
||||
|
||||
destroyRichTextScene( sceneNode );
|
||||
}
|
||||
|
||||
UTEST( UIRichText, WhiteSpaceCollapsePreserveSpaces ) {
|
||||
auto sceneNode = createRichTextScene();
|
||||
ASSERT_TRUE( sceneNode != nullptr );
|
||||
|
||||
String xml = R"xml(
|
||||
<RichText id="rt" white-space-collapse="preserve-spaces">alpha beta
|
||||
gamma</RichText>
|
||||
)xml";
|
||||
|
||||
sceneNode->loadLayoutFromString( xml );
|
||||
UI::UIRichText* rt = sceneNode->find<UI::UIRichText>( "rt" );
|
||||
ASSERT_TRUE( rt != nullptr );
|
||||
|
||||
sceneNode->update( Time::Zero );
|
||||
|
||||
EXPECT_STRINGEQ( richTextRenderedText( *rt->getRichTextPtr() ), "alpha beta gamma" );
|
||||
|
||||
destroyRichTextScene( sceneNode );
|
||||
}
|
||||
|
||||
UTEST( UIRichText, WhiteSpaceCollapsePreserveSpacesNormalizesCRLF ) {
|
||||
auto sceneNode = createRichTextScene();
|
||||
ASSERT_TRUE( sceneNode != nullptr );
|
||||
|
||||
String xml = R"xml(
|
||||
<RichText id="rt" white-space-collapse="preserve-spaces">alpha beta</RichText>
|
||||
)xml";
|
||||
|
||||
sceneNode->loadLayoutFromString( xml );
|
||||
UI::UIRichText* rt = sceneNode->find<UI::UIRichText>( "rt" );
|
||||
ASSERT_TRUE( rt != nullptr );
|
||||
|
||||
sceneNode->update( Time::Zero );
|
||||
|
||||
EXPECT_STRINGEQ( richTextRenderedText( *rt->getRichTextPtr() ), "alpha beta" );
|
||||
|
||||
destroyRichTextScene( sceneNode );
|
||||
}
|
||||
|
||||
UTEST( UIRichText, WhiteSpaceCollapseDiscard ) {
|
||||
auto sceneNode = createRichTextScene();
|
||||
ASSERT_TRUE( sceneNode != nullptr );
|
||||
|
||||
String xml = R"xml(
|
||||
<RichText id="rt" white-space-collapse="discard">alpha beta
|
||||
gamma</RichText>
|
||||
)xml";
|
||||
|
||||
sceneNode->loadLayoutFromString( xml );
|
||||
UI::UIRichText* rt = sceneNode->find<UI::UIRichText>( "rt" );
|
||||
ASSERT_TRUE( rt != nullptr );
|
||||
|
||||
sceneNode->update( Time::Zero );
|
||||
|
||||
EXPECT_STRINGEQ( richTextRenderedText( *rt->getRichTextPtr() ), "alphabetagamma" );
|
||||
|
||||
destroyRichTextScene( sceneNode );
|
||||
}
|
||||
|
||||
UTEST( UIRichText, WhiteSpaceCollapseAcrossInlineSpanBoundaries ) {
|
||||
auto sceneNode = createRichTextScene();
|
||||
ASSERT_TRUE( sceneNode != nullptr );
|
||||
|
||||
String xml = R"xml(
|
||||
<RichText id="rt">alpha <span> beta</span> gamma</RichText>
|
||||
)xml";
|
||||
|
||||
sceneNode->loadLayoutFromString( xml );
|
||||
UI::UIRichText* rt = sceneNode->find<UI::UIRichText>( "rt" );
|
||||
ASSERT_TRUE( rt != nullptr );
|
||||
|
||||
sceneNode->update( Time::Zero );
|
||||
|
||||
EXPECT_STRINGEQ( richTextRenderedText( *rt->getRichTextPtr() ), "alpha beta gamma" );
|
||||
|
||||
destroyRichTextScene( sceneNode );
|
||||
}
|
||||
|
||||
UTEST( RichText, BreakSpacesWrapsAfterPreservedSpacesAndTabs ) {
|
||||
Engine::instance()->createWindow( WindowSettings( 800, 600, "RichText Test",
|
||||
WindowStyle::Default, WindowBackend::Default,
|
||||
32, {}, 1, false, true ) );
|
||||
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
||||
|
||||
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
|
||||
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
||||
ASSERT_TRUE( font->loaded() );
|
||||
FontFamily::loadFromRegular( font );
|
||||
|
||||
FontStyleConfig style;
|
||||
style.Font = font;
|
||||
style.CharacterSize = 16;
|
||||
auto textWidth = [&style]( const String& string, Uint32 tabWidth = 8 ) {
|
||||
return Text::getTextWidth( string, style, tabWidth, TextHints::AllAscii );
|
||||
};
|
||||
|
||||
RichText spacedText;
|
||||
spacedText.setFontStyleConfig( style );
|
||||
spacedText.setWhiteSpaceWrapMode( RichText::WhiteSpaceWrapMode::BreakSpaces );
|
||||
spacedText.addSpan( "aa bb" );
|
||||
spacedText.setMaxWidth( textWidth( "aa " ) + 1.f );
|
||||
spacedText.getSize();
|
||||
ASSERT_GE( spacedText.getLines().size(), (size_t)2 );
|
||||
EXPECT_STRINGEQ( richTextLineText( spacedText, 0 ), "aa " );
|
||||
EXPECT_STRINGEQ( richTextLineText( spacedText, 1 ), "bb" );
|
||||
EXPECT_GT( spacedText.getLines()[0].width, textWidth( "aa" ) );
|
||||
|
||||
RichText onlySpaces;
|
||||
onlySpaces.setFontStyleConfig( style );
|
||||
onlySpaces.setWhiteSpaceWrapMode( RichText::WhiteSpaceWrapMode::BreakSpaces );
|
||||
onlySpaces.addSpan( " " );
|
||||
onlySpaces.setMaxWidth( textWidth( " " ) + 1.f );
|
||||
onlySpaces.getSize();
|
||||
ASSERT_GE( onlySpaces.getLines().size(), (size_t)2 );
|
||||
EXPECT_STRINGEQ( richTextLineText( onlySpaces, 0 ), " " );
|
||||
EXPECT_STRINGEQ( richTextLineText( onlySpaces, 1 ), " " );
|
||||
|
||||
RichText tabText;
|
||||
tabText.setFontStyleConfig( style );
|
||||
tabText.setTabWidth( 4 );
|
||||
tabText.setWhiteSpaceWrapMode( RichText::WhiteSpaceWrapMode::BreakSpaces );
|
||||
tabText.addSpan( "a\tb" );
|
||||
tabText.setMaxWidth( textWidth( "a\t", 4 ) + 1.f );
|
||||
tabText.getSize();
|
||||
ASSERT_GE( tabText.getLines().size(), (size_t)2 );
|
||||
EXPECT_STRINGEQ( richTextLineText( tabText, 0 ), "a\t" );
|
||||
EXPECT_STRINGEQ( richTextLineText( tabText, 1 ), "b" );
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIHTMLTable, basicLayout ) {
|
||||
auto sceneNode = createRichTextScene();
|
||||
ASSERT_TRUE( sceneNode != nullptr );
|
||||
|
||||
@@ -14,14 +14,18 @@
|
||||
#include <eepp/ui/tools/htmlformatter.hpp>
|
||||
#include <eepp/ui/tools/uiwidgetinspector.hpp>
|
||||
#include <eepp/ui/uicheckbox.hpp>
|
||||
#include <eepp/ui/uicodeeditor.hpp>
|
||||
#include <eepp/ui/uihtmldetails.hpp>
|
||||
#include <eepp/ui/uihtmlinput.hpp>
|
||||
#include <eepp/ui/uihtmltable.hpp>
|
||||
#include <eepp/ui/uihtmltextarea.hpp>
|
||||
#include <eepp/ui/uihtmltextinput.hpp>
|
||||
#include <eepp/ui/uimarkdownview.hpp>
|
||||
#include <eepp/ui/uinodedrawable.hpp>
|
||||
#include <eepp/ui/uiradiobutton.hpp>
|
||||
#include <eepp/ui/uirichtext.hpp>
|
||||
#include <eepp/ui/uiscenenode.hpp>
|
||||
#include <eepp/ui/uitextnode.hpp>
|
||||
#include <eepp/ui/uitextspan.hpp>
|
||||
#include <eepp/ui/uitheme.hpp>
|
||||
#include <eepp/ui/uithememanager.hpp>
|
||||
@@ -48,6 +52,9 @@ static void init_ui_test() {
|
||||
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
|
||||
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
||||
FontFamily::loadFromRegular( font );
|
||||
FontTrueType* monospace = FontTrueType::New( "monospace" );
|
||||
monospace->loadFromFile( "../assets/fonts/DejaVuSansMono.ttf" );
|
||||
FontFamily::loadFromRegular( monospace );
|
||||
|
||||
UI::UISceneNode* sceneNode = UI::UISceneNode::New();
|
||||
SceneManager::instance()->add( sceneNode );
|
||||
@@ -56,6 +63,36 @@ static void init_ui_test() {
|
||||
themeManager->setDefaultFont( font );
|
||||
}
|
||||
|
||||
static String uiHtmlRenderedText( const RichText& richText ) {
|
||||
String text;
|
||||
const auto& lines = richText.getLines();
|
||||
for ( size_t lineIndex = 0; lineIndex < lines.size(); ++lineIndex ) {
|
||||
if ( lineIndex > 0 )
|
||||
text += '\n';
|
||||
for ( const auto& span : lines[lineIndex].spans ) {
|
||||
if ( span.type == RichText::RenderSpan::Type::Text && span.text )
|
||||
text += span.text->getString();
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
static String uiHtmlLineText( const RichText& richText, size_t lineIndex ) {
|
||||
String text;
|
||||
const auto& lines = richText.getLines();
|
||||
if ( lineIndex >= lines.size() )
|
||||
return text;
|
||||
for ( const auto& span : lines[lineIndex].spans ) {
|
||||
if ( span.type == RichText::RenderSpan::Type::Text && span.text )
|
||||
text += span.text->getString();
|
||||
}
|
||||
while ( !text.empty() && ( text.front() == '\n' || text.front() == '\r' ) )
|
||||
text = text.substr( 1 );
|
||||
while ( !text.empty() && ( text.back() == '\n' || text.back() == '\r' ) )
|
||||
text.pop_back();
|
||||
return text;
|
||||
}
|
||||
|
||||
UTEST( UIHTMLTable, complexLayout ) {
|
||||
auto win = Engine::instance()->createWindow(
|
||||
WindowSettings( 1024, 653, "HTML Tables Test", WindowStyle::Default, WindowBackend::Default,
|
||||
@@ -348,8 +385,8 @@ UTEST( UIHTML, redditOldThreadWebViewSmoke ) {
|
||||
<< ") "
|
||||
<< "header=(" << headerPos.x << "," << headerPos.y << " "
|
||||
<< header->asType<UIWidget>()->getPixelsSize().getWidth() << "x"
|
||||
<< header->asType<UIWidget>()->getPixelsSize().getHeight() << " offset="
|
||||
<< header->asType<UIWidget>()->getPixelsContentOffset().Left << ","
|
||||
<< header->asType<UIWidget>()->getPixelsSize().getHeight()
|
||||
<< " offset=" << header->asType<UIWidget>()->getPixelsContentOffset().Left << ","
|
||||
<< header->asType<UIWidget>()->getPixelsContentOffset().Top << ","
|
||||
<< header->asType<UIWidget>()->getPixelsContentOffset().Right << ","
|
||||
<< header->asType<UIWidget>()->getPixelsContentOffset().Bottom << ") "
|
||||
@@ -621,6 +658,322 @@ UTEST( UIHTML, WhiteSpaceNowrapContinuesInlineContentAfterOverflow ) {
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIHTML, WhiteSpaceCollapsePreCodePreservesIndentation ) {
|
||||
init_ui_test();
|
||||
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
||||
|
||||
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
||||
<body>
|
||||
<pre id="pre"><code id="code">if (x) {
|
||||
return 1;
|
||||
}</code></pre>
|
||||
</body>
|
||||
)html" ) );
|
||||
SceneManager::instance()->update();
|
||||
|
||||
auto* pre = sceneNode->getRoot()->find( "pre" )->asType<UIRichText>();
|
||||
auto* code = sceneNode->getRoot()->find( "code" );
|
||||
ASSERT_TRUE( pre != nullptr );
|
||||
ASSERT_TRUE( code != nullptr );
|
||||
|
||||
EXPECT_FALSE( code->isType( UI_TYPE_CODEEDITOR ) );
|
||||
EXPECT_GE( pre->getRichTextPtr()->getLines().size(), (size_t)3 );
|
||||
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 0 ), "if (x) {" );
|
||||
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 1 ), " return 1;" );
|
||||
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 2 ), "}" );
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIHTML, PreCodeSimpleFixtureKeepsCompactCodeLines ) {
|
||||
init_ui_test();
|
||||
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
||||
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
||||
|
||||
std::string html;
|
||||
ASSERT_TRUE( FileSystem::fileGet( "assets/html/pre.code.html", html ) );
|
||||
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
||||
SceneManager::instance()->update();
|
||||
|
||||
UIRichText* pre = nullptr;
|
||||
Node* code = nullptr;
|
||||
sceneNode->getRoot()->forEachNode( [&pre, &code]( Node* node ) {
|
||||
if ( node->isWidget() && node->asType<UIWidget>()->getElementTag() == "pre" )
|
||||
pre = node->asType<UIRichText>();
|
||||
if ( node->isWidget() && node->asType<UIWidget>()->getElementTag() == "code" )
|
||||
code = node;
|
||||
} );
|
||||
ASSERT_TRUE( pre != nullptr );
|
||||
ASSERT_TRUE( code != nullptr );
|
||||
EXPECT_FALSE( code->isType( UI_TYPE_CODEEDITOR ) );
|
||||
auto* codeSpan = code->asType<UITextSpan>();
|
||||
ASSERT_TRUE( codeSpan != nullptr );
|
||||
|
||||
const auto& lines = pre->getRichTextPtr()->getLines();
|
||||
ASSERT_GE( lines.size(), (size_t)3 );
|
||||
ASSERT_LE( lines.size(), (size_t)4 );
|
||||
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 0 ), "void main() {" );
|
||||
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 1 ), " printf(\"Hello World\");" );
|
||||
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 2 ), "}" );
|
||||
if ( lines.size() == 4 )
|
||||
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 3 ), "" );
|
||||
ASSERT_FALSE( lines[1].spans.empty() );
|
||||
ASSERT_TRUE( lines[1].spans[0].text != nullptr );
|
||||
ASSERT_FALSE( lines[1].spans[0].text->getString().empty() );
|
||||
EXPECT_TRUE( lines[1].spans[0].text->getString()[0] != '\n' );
|
||||
ASSERT_FALSE( lines[2].spans.empty() );
|
||||
ASSERT_TRUE( lines[2].spans[0].text != nullptr );
|
||||
ASSERT_FALSE( lines[2].spans[0].text->getString().empty() );
|
||||
EXPECT_TRUE( lines[2].spans[0].text->getString()[0] != '\n' );
|
||||
|
||||
ASSERT_EQ( codeSpan->getHitBoxes().size(), (size_t)3 );
|
||||
Float expectedLineHeight = pre->getLineHeightPx();
|
||||
if ( expectedLineHeight <= 0.f ) {
|
||||
for ( const auto& span : lines[0].spans ) {
|
||||
if ( span.type == RichText::RenderSpan::Type::Text && span.text &&
|
||||
!span.text->getString().empty() ) {
|
||||
const auto& fontStyle = span.text->getFontStyleConfig();
|
||||
ASSERT_TRUE( fontStyle.Font != nullptr );
|
||||
expectedLineHeight = fontStyle.Font->getLineSpacing( fontStyle.CharacterSize );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ASSERT_GT( expectedLineHeight, 0.f );
|
||||
Float compactLineLimit = expectedLineHeight * 1.25f;
|
||||
for ( size_t i = 1; i < 3; ++i ) {
|
||||
Float lineDelta = lines[i].y - lines[i - 1].y;
|
||||
EXPECT_LE( lineDelta, compactLineLimit );
|
||||
EXPECT_LE( lines[i].height, compactLineLimit );
|
||||
|
||||
const Rectf& prev = codeSpan->getHitBoxes()[i - 1];
|
||||
const Rectf& cur = codeSpan->getHitBoxes()[i];
|
||||
EXPECT_NEAR( cur.Top - prev.Top, lineDelta, 1.f );
|
||||
EXPECT_LE( cur.Top - prev.Top, compactLineLimit );
|
||||
}
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIHTML, PreCodeFixtureDiscardsFinalNewlineBeforeEndTag ) {
|
||||
init_ui_test();
|
||||
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
||||
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
||||
|
||||
std::string html;
|
||||
ASSERT_TRUE( FileSystem::fileGet( "assets/html/pre.code.2.html", html ) );
|
||||
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
||||
SceneManager::instance()->update();
|
||||
|
||||
UIRichText* pre = nullptr;
|
||||
Node* code = nullptr;
|
||||
sceneNode->getRoot()->forEachNode( [&pre, &code]( Node* node ) {
|
||||
if ( node->isWidget() && node->asType<UIWidget>()->getElementTag() == "pre" )
|
||||
pre = node->asType<UIRichText>();
|
||||
if ( node->isWidget() && node->asType<UIWidget>()->getElementTag() == "code" )
|
||||
code = node;
|
||||
} );
|
||||
ASSERT_TRUE( pre != nullptr );
|
||||
ASSERT_TRUE( code != nullptr );
|
||||
EXPECT_FALSE( code->isType( UI_TYPE_CODEEDITOR ) );
|
||||
|
||||
const auto& lines = pre->getRichTextPtr()->getLines();
|
||||
ASSERT_EQ( lines.size(), (size_t)3 );
|
||||
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 0 ), "int x = 42;" );
|
||||
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 1 ), "const char* y = \"hello\";" );
|
||||
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 2 ), "int z = foo(x, y);" );
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIHTML, PreCodeUsesCodeEditorOnlyForMarkdownAncestorOrGlobalOptIn ) {
|
||||
init_ui_test();
|
||||
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
||||
|
||||
sceneNode->loadLayoutFromString( R"xml(
|
||||
<markdownview id="md">
|
||||
<pre id="md_pre"><code id="md_code" class="language-cpp">void main() {
|
||||
printf("Hello World");
|
||||
}</code></pre>
|
||||
</markdownview>
|
||||
)xml" );
|
||||
SceneManager::instance()->update();
|
||||
|
||||
auto* markdownCode = sceneNode->getRoot()->find( "md_code" );
|
||||
ASSERT_TRUE( markdownCode != nullptr );
|
||||
ASSERT_TRUE( markdownCode->isType( UI_TYPE_CODEEDITOR ) );
|
||||
EXPECT_TRUE( markdownCode->asType<UICodeEditor>()->getDocument().getText().find( "printf" ) !=
|
||||
String::InvalidPos );
|
||||
|
||||
Engine::destroySingleton();
|
||||
|
||||
init_ui_test();
|
||||
sceneNode = SceneManager::instance()->getUISceneNode();
|
||||
UIRichText::setUseCodeEditorForPreCodeBlocks( true );
|
||||
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
||||
<body>
|
||||
<pre id="pre"><code id="code">int value = 1;</code></pre>
|
||||
</body>
|
||||
)html" ) );
|
||||
SceneManager::instance()->update();
|
||||
|
||||
auto* optInCode = sceneNode->getRoot()->find( "code" );
|
||||
ASSERT_TRUE( optInCode != nullptr );
|
||||
EXPECT_TRUE( optInCode->isType( UI_TYPE_CODEEDITOR ) );
|
||||
UIRichText::setUseCodeEditorForPreCodeBlocks( false );
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIHTML, PreCodeBlockFixtureKeepsCompactCodeLines ) {
|
||||
init_ui_test();
|
||||
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
||||
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
||||
|
||||
std::string html;
|
||||
ASSERT_TRUE( FileSystem::fileGet( "assets/html/pre_code_block.html", html ) );
|
||||
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
||||
SceneManager::instance()->update();
|
||||
|
||||
UIRichText* pre = nullptr;
|
||||
sceneNode->getRoot()->forEachNode( [&pre]( Node* node ) {
|
||||
if ( pre == nullptr && node->isWidget() &&
|
||||
node->asType<UIWidget>()->getElementTag() == "pre" )
|
||||
pre = node->asType<UIRichText>();
|
||||
} );
|
||||
ASSERT_TRUE( pre != nullptr );
|
||||
|
||||
const auto& lines = pre->getRichTextPtr()->getLines();
|
||||
ASSERT_EQ( lines.size(), (size_t)19 );
|
||||
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 0 ), "let stepper () =" );
|
||||
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 1 ),
|
||||
" // Execute a single instruction" );
|
||||
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 18 ), " mCycles " );
|
||||
|
||||
Float expectedLineHeight = pre->getLineHeightPx();
|
||||
ASSERT_GT( expectedLineHeight, 0.f );
|
||||
Float compactLineLimit = expectedLineHeight * 1.25f;
|
||||
for ( size_t i = 1; i < lines.size(); ++i ) {
|
||||
EXPECT_LE( lines[i].y - lines[i - 1].y, compactLineLimit );
|
||||
EXPECT_LE( lines[i].height, compactLineLimit );
|
||||
}
|
||||
|
||||
std::vector<UITextSpan*> codeLineSpans;
|
||||
for ( size_t i = 1; i <= 19; ++i ) {
|
||||
auto* lineSpan = sceneNode->getRoot()->find<UITextSpan>( String::format( "cb1-%zu", i ) );
|
||||
ASSERT_TRUE( lineSpan != nullptr );
|
||||
ASSERT_EQ( lineSpan->getHitBoxes().size(), (size_t)1 );
|
||||
EXPECT_LE( lineSpan->getHitBoxes()[0].getHeight(), compactLineLimit );
|
||||
codeLineSpans.push_back( lineSpan );
|
||||
}
|
||||
for ( size_t i = 1; i < codeLineSpans.size(); ++i ) {
|
||||
const Rectf& prev = codeLineSpans[i - 1]->getHitBoxes()[0];
|
||||
const Rectf& cur = codeLineSpans[i]->getHitBoxes()[0];
|
||||
Float prevTop = codeLineSpans[i - 1]->getPixelsPosition().y + prev.Top;
|
||||
Float curTop = codeLineSpans[i]->getPixelsPosition().y + cur.Top;
|
||||
EXPECT_NEAR( curTop - prevTop, lines[i].y - lines[i - 1].y, 1.f );
|
||||
EXPECT_LE( curTop - prevTop, compactLineLimit );
|
||||
}
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIHTML, WhiteSpaceCollapsePreLinePreservesBreaksOnly ) {
|
||||
init_ui_test();
|
||||
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
||||
|
||||
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
||||
<body>
|
||||
<div id="preline" style="white-space: pre-line">alpha beta
|
||||
gamma</div>
|
||||
</body>
|
||||
)html" ) );
|
||||
SceneManager::instance()->update();
|
||||
|
||||
auto* preline = sceneNode->getRoot()->find( "preline" )->asType<UIRichText>();
|
||||
ASSERT_TRUE( preline != nullptr );
|
||||
|
||||
EXPECT_EQ( preline->getRichTextPtr()->getLines().size(), (size_t)2 );
|
||||
EXPECT_STRINGEQ( uiHtmlLineText( *preline->getRichTextPtr(), 0 ), "alpha beta" );
|
||||
EXPECT_STRINGEQ( uiHtmlLineText( *preline->getRichTextPtr(), 1 ), " gamma" );
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIHTML, WhiteSpaceCollapseBreakSpacesAffectsIntrinsicWidth ) {
|
||||
init_ui_test();
|
||||
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
||||
|
||||
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
||||
<body>
|
||||
<div id="normal" style="white-space: pre-wrap; font-size: 16px"> </div>
|
||||
<div id="breakspaces" style="white-space: break-spaces; font-size: 16px"> </div>
|
||||
</body>
|
||||
)html" ) );
|
||||
SceneManager::instance()->update();
|
||||
|
||||
auto* normal = sceneNode->getRoot()->find( "normal" )->asType<UIRichText>();
|
||||
auto* breakSpaces = sceneNode->getRoot()->find( "breakspaces" )->asType<UIRichText>();
|
||||
ASSERT_TRUE( normal != nullptr );
|
||||
ASSERT_TRUE( breakSpaces != nullptr );
|
||||
|
||||
EXPECT_LT( normal->getMinIntrinsicWidth(), breakSpaces->getMinIntrinsicWidth() );
|
||||
EXPECT_GT( breakSpaces->getMinIntrinsicWidth(), 0.f );
|
||||
EXPECT_STRINGEQ( uiHtmlRenderedText( *breakSpaces->getRichTextPtr() ), " " );
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIHTML, WhiteSpaceCollapsePreservedTabsUseTabSize ) {
|
||||
init_ui_test();
|
||||
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
||||
|
||||
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
||||
<body>
|
||||
<pre id="tab2" style="font-family: monospace; font-size: 16px; tab-size: 2">a b</pre>
|
||||
<pre id="tab8" style="font-family: monospace; font-size: 16px; tab-size: 8">a b</pre>
|
||||
<pre id="tabpx" style="font-family: monospace; font-size: 16px; tab-size: 32px">a b</pre>
|
||||
</body>
|
||||
)html" ) );
|
||||
SceneManager::instance()->update();
|
||||
|
||||
auto* tab2 = sceneNode->getRoot()->find( "tab2" )->asType<UIRichText>();
|
||||
auto* tab8 = sceneNode->getRoot()->find( "tab8" )->asType<UIRichText>();
|
||||
auto* tabpx = sceneNode->getRoot()->find( "tabpx" )->asType<UIRichText>();
|
||||
ASSERT_TRUE( tab2 != nullptr );
|
||||
ASSERT_TRUE( tab8 != nullptr );
|
||||
ASSERT_TRUE( tabpx != nullptr );
|
||||
|
||||
ASSERT_EQ( tab2->getRichTextPtr()->getLines().size(), (size_t)1 );
|
||||
ASSERT_EQ( tab8->getRichTextPtr()->getLines().size(), (size_t)1 );
|
||||
ASSERT_EQ( tabpx->getRichTextPtr()->getLines().size(), (size_t)1 );
|
||||
EXPECT_STRINGEQ( uiHtmlLineText( *tab2->getRichTextPtr(), 0 ), "a\tb" );
|
||||
EXPECT_STRINGEQ( uiHtmlLineText( *tab8->getRichTextPtr(), 0 ), "a\tb" );
|
||||
EXPECT_STRINGEQ( uiHtmlLineText( *tabpx->getRichTextPtr(), 0 ), "a\tb" );
|
||||
EXPECT_EQ( tab2->getTabSize(), 2u );
|
||||
EXPECT_EQ( tab8->getTabSize(), 8u );
|
||||
EXPECT_GT( tabpx->getTabSize(), 2u );
|
||||
|
||||
const Float tab2Width = tab2->getRichTextPtr()->getLines()[0].width;
|
||||
const Float tab8Width = tab8->getRichTextPtr()->getLines()[0].width;
|
||||
const Float tabpxWidth = tabpx->getRichTextPtr()->getLines()[0].width;
|
||||
EXPECT_GT( tab8Width, tab2Width );
|
||||
EXPECT_GT( tabpxWidth, tab2Width );
|
||||
|
||||
ASSERT_FALSE( tab2->getRichTextPtr()->getLines()[0].spans.empty() );
|
||||
ASSERT_FALSE( tab8->getRichTextPtr()->getLines()[0].spans.empty() );
|
||||
ASSERT_FALSE( tabpx->getRichTextPtr()->getLines()[0].spans.empty() );
|
||||
ASSERT_TRUE( tab2->getRichTextPtr()->getLines()[0].spans[0].text != nullptr );
|
||||
ASSERT_TRUE( tab8->getRichTextPtr()->getLines()[0].spans[0].text != nullptr );
|
||||
ASSERT_TRUE( tabpx->getRichTextPtr()->getLines()[0].spans[0].text != nullptr );
|
||||
EXPECT_EQ( tab2->getRichTextPtr()->getLines()[0].spans[0].text->getTabWidth(), 2u );
|
||||
EXPECT_EQ( tab8->getRichTextPtr()->getLines()[0].spans[0].text->getTabWidth(), 8u );
|
||||
EXPECT_EQ( tabpx->getRichTextPtr()->getLines()[0].spans[0].text->getTabWidth(),
|
||||
tabpx->getTabSize() );
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIRichText, anchorMargins ) {
|
||||
auto win = Engine::instance()->createWindow(
|
||||
WindowSettings( 800, 600, "Anchor Margins Test", WindowStyle::Default,
|
||||
@@ -2589,8 +2942,7 @@ UTEST( UIHTML, BodyHeightMiscalculationFixture ) {
|
||||
|
||||
// With justify-content: space-between in a row-direction flex:
|
||||
// .brand should be at the left side of .wrap (near padding edge).
|
||||
EXPECT_LT( brandWidget->getPixelsPosition().x,
|
||||
wrapWidget->getPixelsSize().getWidth() * 0.25f );
|
||||
EXPECT_LT( brandWidget->getPixelsPosition().x, wrapWidget->getPixelsSize().getWidth() * 0.25f );
|
||||
|
||||
// .links should be at the right side of .wrap, NOT next to .brand.
|
||||
Float wrapW = wrapWidget->getPixelsSize().getWidth();
|
||||
@@ -3001,7 +3353,7 @@ UTEST( UIHTML, AnchorsSizing ) {
|
||||
continue;
|
||||
EXPECT_GT( a->getPixelsSize().getHeight(), 0 );
|
||||
if ( !a->getText().empty() && a->getFontStyleConfig().Font ) {
|
||||
String text = a->getText();
|
||||
String text = UIRichText::collapseInternalWhitespace( a->getText() );
|
||||
text.trim();
|
||||
if ( !text.empty() )
|
||||
EXPECT_GE( a->getPixelsSize().getWidth(),
|
||||
|
||||
@@ -954,7 +954,7 @@ UTEST( UITextNode_EdgeCases, EmptyTextNodesDontAffectLayout ) {
|
||||
if ( child ) {
|
||||
EXPECT_TRUE( child->isTextNode() );
|
||||
UITextNode* textNode = static_cast<UITextNode*>( child );
|
||||
EXPECT_TRUE( textNode->getText().empty() || String::trim( textNode->getText() ).empty() );
|
||||
EXPECT_EQ( textNode->getLayoutCharCount(), (size_t)0 );
|
||||
}
|
||||
|
||||
destroyRichTextScene( sceneNode );
|
||||
|
||||
Reference in New Issue
Block a user