From e1d9642dc6cf8a5fb4ae0832614c8f034cc0e769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Wed, 29 Apr 2026 00:28:11 -0300 Subject: [PATCH] Simulate content-box for HTML compat layer. --- .agent/plans/border_box_model_plan.md | 286 ++++++++++++++++++ .../assets/html/eepp-ui-anchor-margins.webp | Bin 1478 -> 1476 bytes .../assets/html/eepp-ui-border-rendering.webp | Bin 7522 -> 8208 bytes include/eepp/ui/uiborderdrawable.hpp | 6 +- include/eepp/ui/uiwidget.hpp | 9 + src/eepp/ui/blocklayouter.cpp | 37 +-- src/eepp/ui/tablelayouter.cpp | 26 +- src/eepp/ui/uiborderdrawable.cpp | 7 +- src/eepp/ui/uihtmlwidget.cpp | 2 +- src/eepp/ui/uirichtext.cpp | 43 +-- src/eepp/ui/uiwidget.cpp | 46 ++- 11 files changed, 393 insertions(+), 69 deletions(-) create mode 100644 .agent/plans/border_box_model_plan.md diff --git a/.agent/plans/border_box_model_plan.md b/.agent/plans/border_box_model_plan.md new file mode 100644 index 000000000..6fcee76ef --- /dev/null +++ b/.agent/plans/border_box_model_plan.md @@ -0,0 +1,286 @@ +# Border Box Model Content Offset Plan + +## Problem Statement + +In browser engines, content (text, child widgets) is positioned at `border-width + padding` from the element's edge. In eepp's GUI system, borders are `BorderType::Inside` by default — a pure visual decoration drawn OVER the padding/content area. Content is only offset by padding, ignoring border width entirely. This causes HTML widgets to render text misaligned compared to real browsers. + +**User requirement:** UIHTMLWidget elements must behave like browsers — the border should consume space and content should be offset by border + padding. + +--- + +## Key Concepts + +| Concept | eepp Current | Browser/CSS | Desired | +|---------|-------------|-------------|---------| +| Border space | 0 (Inside type) | border-width | border-width | +| Content offset | padding only | border + padding | border + padding | +| `box-sizing` | not implemented | `content-box` (default) | add `content-box`/`border-box` | + +**BorderType behavior:** +- `Inside` (default): border drawn inside element; `getBorderBoxDiff()` returns zero rect; no space consumed +- `Outside`: border extends outward from element; adds space outside +- `Outline`: border centered on element edge; half inside, half outside + +For HTML compatibility, we treat borders as **space-consuming** regardless of BorderType — they push content inward by their width. This matches the CSS `content-box` model where `width` specifies the content area. + +--- + +## Scope: What Changes + +### 1. Add helper method to UINode/UIWidget + +**File:** `include/eepp/ui/uiwidget.hpp` (declaration), `src/eepp/ui/uiwidget.cpp` (implementation) + +Add `getPixelsContentOffset()` that returns a `Rectf` containing `padding + border` for all 4 sides. This becomes the single source of truth for content positioning in HTML widgets. + +```cpp +// Returns the content area origin offset = padding + border +Rectf getPixelsContentOffset() const; +``` + +Implementation: +```cpp +Rectf UIWidget::getPixelsContentOffset() const { + Rectf offset = getPixelsPadding(); + if (hasBorder()) { + const auto& b = getBorder()->getBorders(); + offset.Left += b.left.width; + offset.Right += b.right.width; + offset.Top += b.top.width; + offset.Bottom += b.bottom.width; + } + return offset; +} +``` + +**Complexity: LOW** — one new method, ~15 lines. + +--- + +### 2. BlockLayouter — Content Area Calculations + +**File:** `src/eepp/ui/blocklayouter.cpp` + +All locations that use `mContainer->getPixelsPadding()` for child positioning must switch to `mContainer->getPixelsContentOffset()`. + +**Affected lines (~8 sites):** + +| Line(s) | Current | Change | +|---------|---------|--------| +| 34-43 `computeIntrinsicWidths` | `getPixelsPadding().Left + .Right` | add border widths to intrinsic size | +| 74-77 `updateLayout` totW | `getPixelsPadding().Left + .Right` | `getPixelsContentOffset().Left + .Right` | +| 88-92 `updateLayout` totH | `getPixelsPadding().Top + .Bottom` | `getPixelsContentOffset().Top + .Bottom` | +| 161-167 `positionRichTextChildren` hitbox | `getPixelsPadding().Left/Top` | `getPixelsContentOffset().Left/Top` | +| 214-216 BR element width | `getPixelsPadding().Left + .Right` | `getPixelsContentOffset().Left + .Right` | +| 227-228 custom widget position | `getPixelsPadding().Left/Top` | `getPixelsContentOffset().Left/Top` | + +**Complexity: MEDIUM** — mechanical replacement, ~8 call sites. + +--- + +### 3. UIRichText — Text Rendering & Intrinsic Widths + +**File:** `src/eepp/ui/uirichtext.cpp` + +| Line(s) | Current | Change | +|---------|---------|--------| +| 180-186 `draw()` | `mScreenPos + mPaddingPx.Left/Top` | add border width to offset | +| 589-590 `rebuildRichText` maxWidth | `- getPixelsPadding().Left - .Right` | `- getPixelsContentOffset().Left - .Right` | +| 638-641 block child width | `- getPixelsPadding().Left - .Right` | `- getPixelsContentOffset().Left - .Right` | +| 665-668 child size computation | same pattern | same | +| 725-728 `getMinIntrinsicWidth` | `+ mPaddingPx.Left + .Right` | `+ getPixelsContentOffset().Left + .Right` | +| 753-756 `getMaxIntrinsicWidth` | same | same | + +**Complexity: MEDIUM** — ~6 call sites, same mechanical pattern. + +--- + +### 4. UIHTMLWidget — Out-of-Flow Children + +**File:** `src/eepp/ui/uihtmlwidget.cpp` + +| Line | Current | Change | +|------|---------|--------| +| 202 `positionOutOfFlowChildren` | `getPixelsPadding().Left, .Top` | `getPixelsContentOffset().Left, .Top` | + +Container block origin for absolutely positioned children must include border. + +**Complexity: LOW** — single line change. + +--- + +### 5. TableLayouter — Table Layout + +**File:** `src/eepp/ui/tablelayouter.cpp` + +| Line(s) | Current | Change | +|---------|---------|--------| +| 274-277 `computeIntrinsicWidths` | `getPixelsPadding().Left + .Right` | `getPixelsContentOffset().Left + .Right` | +| 309 available width | `getPixelsPadding().Left + .Right` | `getPixelsContentOffset().Left + .Right` | +| 513, 516 row positioning | `getPixelsPadding().Left` | `getPixelsContentOffset().Left` | +| 527-529 wrap-content height | `getPixelsPadding().Top + .Bottom` | `getPixelsContentOffset().Top + .Bottom` | + +**Complexity: LOW** — ~4 call sites. + +--- + +### 6. UIWidget::getMatchParentWidth/Height + +**File:** `src/eepp/ui/uiwidget.cpp` (lines ~2577-2617) + +These methods calculate how much space a `match_parent` child can use. Currently subtract parent padding; must also subtract parent border. + +```cpp +// Before: +Float width = getParent()->getPixelsSize().getWidth() - marginLeft - marginRight - + padding.Left - padding.Right; +// After: +Rectf parentOffset = getParent()->asType()->getPixelsContentOffset(); +Float width = getParent()->getPixelsSize().getWidth() - marginLeft - marginRight - + parentOffset.Left - parentOffset.Right; +``` + +**Complexity: LOW** — 2 methods, ~4 subtraction lines each. + +--- + +### 7. UIWidget::calculateAutoMargin + +**File:** `src/eepp/ui/uiwidget.cpp` (lines ~590-659) + +`margin: auto` calculation uses parent padding to determine available space. Must include parent border. + +```cpp +// Before: +Float availableWidth = parentSize.getWidth() - parentPadding.Left - parentPadding.Right - + getPixelsSize().getWidth(); +// After: +Rectf parentContentOffset = ...getPixelsContentOffset(); +Float availableWidth = parentSize.getWidth() - parentContentOffset.Left - + parentContentOffset.Right - getPixelsSize().getWidth(); +``` + +**Complexity: LOW** — ~4 call sites. + +--- + +### 8. (Optional) CSS `box-sizing` Property + +**Scope:** Can be deferred. Adding it now would make the fix more complete but doubles the complexity. + +If implemented: +- Add `BoxSizing` to `PropertyId` enum (`propertydefinition.hpp`) +- Register property: `registerProperty("box-sizing", "content-box")` +- Under `content-box`: width/height set on content area; border+padding added outside (current plan) +- Under `border-box`: width/height include border+padding; content = width - padding - border (would need reverse calculation) + +**Complexity: HIGH** — new property, two calculation modes, affects all width/height resolution. Recommended as follow-up. + +--- + +## Non-Scope / NOT Changing + +- **Non-HTML widgets** (UIPushButton, UITextInput, etc.) — they continue using `getPixelsPadding()` directly and border remains decorative. +- **UINode::nodeDraw()` clip regions** — the clipping pipeline already uses `getBorderBoxDiff()` for BorderBox clip; no change needed. +- **UIBorderDrawable rendering** — border geometry generation is unchanged. +- **BorderType behavior** — Inside/Outside/Outline remain as-is; we only USE the border width value for content offset, regardless of type. +- **Background rendering** — backgrounds already render within the padded area; we're only moving content inward. + +--- + +## Test Impact & Validation Protocol + +### Expected Test Failures + +This change alters the content area origin for all HTML widgets — text, child widgets, intrinsic widths, and match-parent sizing all shift. This means: + +**Guaranteed to fail:** +- `UIBorder.renderingVariations` — text inside bordered boxes will shift inward by the border width, changing pixel positions. **This failure IS the expected correct behavior** (the test proves the fix works). +- `UIRichText.anchorMargins` — content offset changes affect the rendered layout. +- `UIRichText.spanPadding` — spans with padding inside bordered containers shift. +- `UIHTMLTable.complexLayout` (1,2,3) — any elements with borders will have their text/content shifted. + +**Expected to pass unchanged:** +- Non-HTML widget tests (UILayout, FontRendering, etc.) — these widgets don't use the HTML border model. +- Tests where no element has a border — no content offset change occurs. + +**Unknown (may or may not differ):** +- Margin-dependent tests (e.g., `UILayout.marginAuto`) — if parent has a border, `getMatchParentWidth/Height` result changes. +- Layout tests with nested containers — cascading size changes from border inclusion could alter layouts. + +### What "Re-generate and Verify" Means + +The `compareImages` helper in the unit tests works as follows: + +1. **Golden image check:** On each test run, the rendered frame is captured via `win->getFrontBufferImage()` and pixel-compared against a stored `.webp` image at `bin/unit_tests/assets//.webp`. + +2. **Auto-generation on first run:** If the golden image file does not exist, the captured frame is saved AS the new golden image and the test passes. This is how `eepp-ui-border-rendering.webp` was created. + +3. **Re-generation for updated rendering:** To update a golden image after an intentional rendering change: + ```bash + # Delete the old golden image, re-run the test to auto-create a new one + rm bin/unit_tests/assets/html/eepp-ui-table-complex.webp + ASAN_OPTIONS=detect_leaks=0 xvfb-run -a -s "-screen 0 1280x1024x24" \ + bin/unit_tests/eepp-unit_tests-debug --filter="UIHTMLTable.complexLayout" + ``` + +4. **Human validation is REQUIRED after re-generation.** The test will pass automatically once the golden image is regenerated, but this proves nothing — it only proves the rendering is consistent with itself. A human must visually inspect the new rendering (against the old golden image, or against a reference browser rendering) to confirm the change is correct and not a regression. The agent can assist by: + - Describing expected visual differences (e.g., "all text should be shifted right by 4px in bordered elements") + - Comparing pixel dimensions between old and new golden images + - Rendering the same HTML in a reference browser for side-by-side comparison (if the agent has image analysis capabilities) + +### Agent Protocol for Failing Tests + +When tests fail due to expected rendering changes, the agent MUST: + +1. **Report** which tests failed and whether the failure is expected (border-related shift) or unexpected (regression). +2. **Do NOT auto-regenerate** golden images without first describing the expected visual differences to the user. +3. **Request human validation** by explaining what changed and asking the user to confirm the new rendering looks correct. Example: *"The UIBorder.renderingVariations test failed because text inside bordered boxes shifted right by border-left-width and down by border-top-width. I'll regenerate the golden image now — please visually verify the result matches expectations."* +4. **Regenerate golden images only after approval** — delete the old `.webp`, re-run the test, and confirm it passes. +5. **Verify with a reference browser** if the agent has image analysis capabilities — render the same HTML in a browser and compare. + +--- + +## Risk Assessment + +| Risk | Severity | Mitigation | +|------|----------|------------| +| Breaking non-HTML widgets | HIGH | Helper method on UIWidget, but only HTML layouters (BlockLayouter, UIRichText, TableLayouter) call it. Non-HTML widgets keep using `getPixelsPadding()` directly. | +| Intrinsic width changes breaking layout | MEDIUM | Run existing HTML layout image tests after each change. Verify pixel-identical rendering with re-generated golden images. | +| Match-parent calculations | MEDIUM | `getMatchParentWidth/Height` is called by ALL widgets, not just HTML. Must gate the border addition on whether parent has a border. | +| Circle dependency on border resolution | LOW | `updateBorders()` is lazy — widths are empty strings until first draw. We must call `mOwner->lengthFromValue(...)` to resolve before reading. In the `getPixelsContentOffset()` method, `getBorder()->getBorders()` accesses already-resolved values — `updateBorders()` is called in `UIBorderDrawable::update()` before draw. | + +--- + +## Implementation Order + +1. **Add `getPixelsContentOffset()` method** to UIWidget (declaration + implementation) +2. **Update BlockLayouter** — switch all `getPixelsPadding()` to `getPixelsContentOffset()` +3. **Update UIRichText** — text rendering offset and intrinsic widths +4. **Update UIHTMLWidget** — out-of-flow children offset +5. **Update TableLayouter** — table cell padding offset +6. **Update `getMatchParentWidth/Height`** — gate on parent having border, subtract parent border +7. **Update `calculateAutoMargin`** — gate on parent having border, subtract parent border +8. **Run all tests** — identify which fail and classify as expected vs unexpected +9. **Request human validation** — for all tests with expected failures, describe the visual change and ask for confirmation +10. **Regenerate golden images after approval** — delete old `.webp` files, re-run tests to capture new baseline +11. **Verify non-HTML widgets unaffected** — ensure non-HTML tests still pass with existing golden images + +--- + +## Verification + +After implementation, the agent must: + +1. **Run the full test suite** and compile a failure report categorizing each as: + - **Expected failure (border shift):** tests where content moved due to border offset — these visually differ from old golden images + - **Unexpected failure:** tests where the change caused a regression — these must be investigated and fixed + - **Passing unchanged:** tests that continue to match their existing golden images + +2. **For each expected failure**, describe to the user exactly what changed (e.g., "text in `UIRichText.anchorMargins` shifted right by the container's border-left-width of 4px"). See [Test Impact & Validation Protocol](#test-impact--validation-protocol). + +3. **Await human approval** before regenerating any golden images. + +4. **After approval**, regenerate golden images for the affected tests and confirm they pass. + +5. **Verify** the `UIBorder.renderingVariations` test now produces the correct browser-like rendering (text inside bordered boxes is properly offset by border + padding). diff --git a/bin/unit_tests/assets/html/eepp-ui-anchor-margins.webp b/bin/unit_tests/assets/html/eepp-ui-anchor-margins.webp index e8e35dabb695385abc3f5e126d90113df2528bf0..ac9873391103a4d7d18c4d31b1bda85561361467 100644 GIT binary patch delta 1225 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 I5*~~U0B9_!a{vGU diff --git a/bin/unit_tests/assets/html/eepp-ui-border-rendering.webp b/bin/unit_tests/assets/html/eepp-ui-border-rendering.webp index d63bb804c161928e60bc0902a22a28e6c29024a5..68a5cd5c8dc5431dc36163896f4250a96c1a2378 100644 GIT binary patch literal 8208 zcmYjWWmH_tvW8&62_AgV;O-D)u;3EhgF9sK!QGwU2@)i@OK>N+ySok$+}|YUo_BB8 zk6rcESG9Vry}P@*w(18dsn--RFq)F$${Nai@{i;&Fsv0l>oDAy0rhrj7kjNIGq4gp z@ZSlDWSx48k~wRZ?c}x#oojz5Y6$3rQ(e@4Hp`%9(HZH^a0Z!Ywoj@c$3DCiGcTk~ zgU!vaO{6&gXLQ3uo-1DND_(TKyrqWUXr7`U6xU;ncbmxxGwsLo!?veu@?Tw0@rgf%g|b1`!u z!fnXZG}}$BTA!{z(P;|tc{wulc>~M9lnsN~2}3(x*S+u{8bPgF|G8NSQ+k;9Y|fWI z2L|@`6`Xqq683A{%WwF$x-P_4{#=UpD)ZzlX@L61S3e@{YwRMq3&*%Qw=m8BGx|)- z3s82!?QXjnz+W;KHd|$#%N;*D-E%b;F@`?|=Ep~B$><}xI)0~O2G$-B4z7#ntWN8} z2+PHTD{IjX%nPQvr65MthIE|3TII03c)eFhZ=OB$UF2xE zt7U~p_KA8qTuZfL1@n68H~G?S2v8+M&@pE`8izS!id#cI=kIizk~P-e{nBLqw&IDm zSB`lIb_D9zxX%wmPOb$83rbQd`k8Rgd$WR7c_b^5Fx(9Cy&@)nuH`LWyYVq|>MW`4 zj1RfL^+`$WArzL46}`=s8Yi$rHydpto@=POUWV6Hm2(WGAr&mTUKE}iPbt?hJDshe z8I&4%l3GHy8}ncRcM)e3h5o=`f;O59T&lu>?3~2_YuBuVB+>2!wV$#c5!qXxhkjo6 zyul~zui9CU7s=7+V)lXYbc__1X>>bVqdaalH~7 zw;&;GTzhSATNJu~yzI>hrZO{|`mELyT#-(>uk-$M`wrs(YP#TSSjKXOBc=CP!tTEL zo8(KCRG@jQ8}+P;FT=&T1l6a7Ew?mwqz5)5yg!0iG|l)R4WsJ-G|E1gmg*3k@7XgE#pg6@E?9zP zkQdyCnPNDJc0qk+p(gK&Xx_9xd7b*u(q%iR4G@y;VepHC5wGq^G(>pcuihv*!} zP7Yy~ZOWFZSVUKd$RmxsG6<4wu+*z`Y+Q%v*y&CVaVQa9a5AsMJp+@>mx;fd#yfPN z7^^tMO@x7gMScUk9YNzoyfT(Ot$>fp+r*-qAeJB%2TPk~);NkNPq;I}{(F)to`9gn z5oa>duqAm-(FmxD@cn<)z(Gq!4d~SW2yd>wD+Z{$JXu|G49g7-m-n5%pz37m(X-*S z8p#6xF!~V0yPrW6t5>+5JWNt?RLXRZjkkquC7Qh0@+ln;f6)=gl&Tt(O5CM6icx17 zI@ip>7@yU>Org2MSZdjXdjE_EsrA&L6bx-Ljy-g^seTT##$3(D7$T2P9+(PmqWfwj zm5{$Fa{GK!o)e2hSH;JU8OezVgC(}cf%7=tFbXexd6M__d>!PmNTaCjB&OU!TpVLf zi)jW|goW7B=}>CI^x6!rAP%V|=RRQ35sBV+qnI}dJxrYFEF`7&=-{f?PDz<~JGHAR z|0<{&mgSbQS7IC1p~zO5So{`Mb48iwHF5~5%r(KP`aVN3Tp~1&XTT!G9;+UdE+%SF z!!vp%p&wjcZ8?0_1M7eVUneG|%qRAaSmKe}W@f4NptKF!887{#>0KT>)r$$StLbWj ztD)Bjk%y_!O2+8npV(*1;oG|qT_EGF&1`W4cuG5~sAODSm45c#_3Amj?(k&QS%Yq$ zp|(-AbOxT=CIF(a_t0LHo>rk+it1}$m9D+ISAu(__3o+-tJn9xBE5z;bQ-u2OOB3|d9PwB<9 z_*f(sFM!yf4gQYBnRed%a^jw4|88FIDTS9R<^nWJLIU_I!PA==BDH>8!px;o6LVvM zMTcc>+?URqHE`hdnD&h#rJZ6{5Z6<+!`BO)Q0nRZ6z{bLh0JRFu9j5qtKR{fBN}rb zkUqEF==9DErqs`{?R*cW-RUR1n3-u>u%RC_e&dj5@Adj-QI1=l-zw+BUFWOjv>p2j z1gfV+c zl{)voN!n%OV%O)uFM_3AK6Kq-c--1rr;W;)lC~G*1pAUm{+-Ug)10|B6i_3E2%I za};`{-IIYjJ20_*YjvFotvprfsvH1h{p`L>T+8M)MC7_QQSXMX&k)+*Du&wKP`Uxh zv^ypG`4J-)9EcxcrwxSIFRgMr7yr$Mwp1Mw>RNN64=fzqaB>*$c4W5ET1S0=5)KHo zg(2vlCQX85rcK1x^NAiqN+I|o0tF0Jo6ep6c+?(_$q~>QV#&2sjbtym)>wXdid%bL zzetok*WOfE^fvi^)G9oO6^nKfI>@RhUQng%5{Ulx5)F(>j-hKk4Vx>J9=CcdRpPiM zf;WMY*mIu*$GCe3JkKma(ROUKHpCY~7!`+`I%DbG34dTSYv;PnSu`vbX#kZ0>l>nS zrcw$*FZ5@280>s~@9ZjSJ#8&9wz#*{-49gkyv;@LRr|=xnNqW7yeM1MlY!ARSm@nKf6n|}9E^4}E= zGaA2!m`L7q20BC9YUtEOh+~DTHA0uLhHDZ|cAA3`TGT?-sYIl#^I!ft*l5#18ZB_h zYTEbzenI&O+uF5$6$mcvOPQX(lpaFWG-()2jjn>JP z5qIIHS?FmY?@f5_G51Prc%+X(W-qsuv%tWZ2GR%+$C~!L8&kkX`49M`T)};{4htKX ze-CHc4|!Ap|9;6*z2d)U;;i43nErik*yZ6O{GNU!OaSGcR{#;)TGKDeAFjm#;CBM2 zI$_8WnHR{`9glmokwLE^IcF_40P1%f+m9r1j`tb+PwWF58PBxtSD@KoNxa6Wk-m9} zEj{KyDX!5O4$FU3aj07KSJgae`}65~z3^*HKeZ#B3LiL<3%h@LwhV7^FM(0aSWEU6 zft46N6+Cw&xPF2sEc0nf6w5qi2+|<_j5?WAvgDCagLj)Gp z@8h-1dKlL!9b12Gtk$fn-#-O~)+VWl?FuL~ENzMPfv2YxR=Ovn8wclvkiaP5s5k!s zBz$;J7P!CDh6M1;KXwYR*j2p6+`V{9M-g{)cWVv_y2IGLz>x%Ti3>j2UI*)@ZrvP@ z!bbF!bK3Y>@7ms$mCF_UhUa73*>+iv6C@(qFK@a4No{6HmO zphZ|YTcS-L#G%x?T25V{fJcW|T4p!zLPp;r{CBCSwX}t z(D6xg6#lhU?L+R8pK15$u-UJVU$F!)d>4rL-5&k6y@p>%F1dFO_!ugv$||OdTpM(U>1s z#AERwDl|^MJ3J&Qj{9|vpgv$)CC=bntp&gn&@PQb$zsxQpMjLzR@_z`Gp>a+iIIp^ zL=w&0cGOshw=}}WSzE>=-tlm;8oVflbGwRcz;zI z+Z)02R@xIU-Za|>EDt0EFP`9h^aiTch@$dkt5gdj#jCmChj?YxNi&jzhLgg2(5bi!~X;Y~pW1E;1KFA)Qv2N3KY}>U~pR!koUXyyihKA}4 z3eL1RO0Jn^dF0~2gscQye>uxOrzy( zv2bzt3RVks+Q?$Nyxrx_|J=;F%Q~%E)+hQkG{ax6()(sK@vocckK3%-NTQi_!A7>^ z*k&+kaR!`Wl#Li{S1VDr*hiA>4z-Zw;v0|=WsFM>{KBl8?>F1GT7Cx^A?CV^v%#qd z0a7))nAjqX-EPPX$PqY&_txzQ$aC)!Q@NJkG&z}suP4CCSzO_!JX0S1jnHxhPf zrB|WCQfEv~K-@y+qAEjQmj`6M1ARD*JChHup$c$r;&jM+v~JZARnv2BJU9uc6z7DX zkP!oJW^A?DANE1n@6)AOqxJt|$xQvAgg-=z@GK$`6$1|v(Q9D*`_4_gyow9^N`!r8 z)V7Kn+^(kGTZi<8JCk9{(B?aEaaf431~{Kq{rrD?FXzlohUNPh#$n$M%P@{iF&Q+H zd!Su`h$?GPkTM>iPGwx9RTs@Kn`2yK={bwaHDJ4AiHIO}VVy8+h#|1TpH7gvQLfF! zxL9M5xe>+v8!lAF_u$v#4#iYfpnPSFBXp(ObeT_FmJ(Pvip5(QC~yR8QR0*Wn^rU# zKVtiq4v&%AY%iDmlI^Ah{R%eMH;sZ}6qp^b@;zue7!{+{l8zWyCdj?9hwdr1v|Y+D zzSC8uI(9nt)1@!{0fmD9xG_j5ag5*ji5zNa9LR0Au$uk^eV1WVJ+%Ue^D9N+k))>4 zM2>pr)J3;OYFGhk8zuGqiJc*+-uDQFWc9MBIlc7OQ=j2%Z_`6(WKPbW-t^ydg^;^fS5j56lPY;9to*yyZxNaiAPeTpH_E?1?$Hf+5;<(__B4PmB zfqwS~?;%NscCjLOq&{%BN_-~KE@I*;cM$jZHt%b?PUCCuz25mTUBW8pYlh-UFj78& zey67YWx{ih@9-)kV?^B`&clitO*V(<7I){;w)@LE8ny5`8;dBn-HxobF^cWf zXGk8DxR8JOE__1Kx|S}?3jbMmWaw*wHZp?@L^d4u<=;t?M(OlRdyasz?Ut$0l{*P8 z94fR+GZ|s1Puf1Fdn3!>NB0^2Tfk9SHfg)iZ%ho~;BriHcT?ziyb_zwEFrk7Oi5(I z!koHsqW0qyl3Jt7NSN;+Z@_sKMq4M=n-fRD5YmS9Zs68NH0vqOb}dfOo{p<%8WCGM z?^>M*xCV#o{K)M$>a&UhURJb(7IIt|!SEB4@pz=Z=-D;oE1%q!6_cI^>*!R8ElE(2 zb3d^yUoSZpetY%Df#$WF#}U6Mx}_|Gg$E3cY7fba^U2Ou7s==-CI{tBsGgI1y`b~K zh`vJqh__U1{Q^%#%T?ia73c>axfvaIy6$JJjCUhly7gSdjZLUJQTcHfmjKC8RcQSd%5lVP1`vaxMv8bOsRR`IP!T+IKLH`WHo0~f@U zfPnV>p8u1I2LabSd%DNhCQ<@CWb#!w%{^wH5|NC6)9RVV_R~ zIU}h=V-)dej1ToS>K@cX*uihKF|$t(Hi9m4+zUJx=))9+nAS1G8UF13yZWO}0Vu1< zr~N}~$*$nCy4z+4AX&Yxz)Rj3!BcS^?O=SKHC?vtJ{dfJv512F1@86no?X5nJJNeS zIc^++SvqK!;`@(r$5ubEtgP;*8{ZoXK5_`@vWDq zJRsa)G{H+#zg7bc@@x)*|49ytKjBxvUdz^C;gq>tp?m4;5>xsum2y_YvcZT}{(QY7 zp)eKiZ?ch1j*_w%(K)jAT&m#0>W~;0XtE)iKpkl8NTB4J@IJqhml}Z7-{tRG-TB2d z>+{26D_+-!zY@#1jQfl#9#hRksnD9-pBt zgg91LoBJG3zzw2*4>214(KHr%eWV@pBE5>sYf4Nlk+Oswz-Y5-SS5}5PB4yWn{v)y zokbR7InFK)OW&UEb$RImdFvy+4+S^%(DH`Y82Um8%xK=m^ZRF&Ur!n&kD}HxI3n8$ zPRVv`{aJjdk9i36+6P(S;J^2Te=gy~ zZ{)FCRs9G(5V0u>iAaPb;Z>Fcr_03B9Oi4}+)*i{h)8lXV8X}^PUhr~G=ad+%}1g7 zT`p)`d=gvc(QN+f-9D>*ewKa2mnr0zuh}kv&0q@gqVPcmU?XStV(P`@k zMRLPZiGMq9Lk?$LN;5V~M68w0@ltOv$ec%f#J1VZ@H^QkUaeT6g1Wv8e>4cfP@1=$wX=>EE_^gbhu;-VAmF}|8CO)2``4_FXyAXrQtFH9D z4~HH0+NWmc#VDFceJ|{-u7;}BV0ZG!EhsVQGs`eelwA+_EzS@?|3CaI(FU~P2Cg--Kn+UH(3Vag4XXiJ(~~&f?IG>( zQqtcc=n3tq#mpy*V@sJ(ONumJD7BP-pK1}h`M(w1MsJX~p?iD9K1TxG^HsKw<9Oq= z^8p?`#8+>$)ZCbU|KGiTg%>`IsFAIl+ol5u z3*$r`b{~a%M!Mnv&ozXETJ<%AQX-V))*{SLymT+8EjFd_`R&<`X)4xHc+u$$Xy2JM zkC!a~!uh7XXCUV^T$V$8+=pTFizWXtX=X;$yGyD82AwVrDdpE8vU<-u3_BLp8gm4W zYFjzK*v)Pv=>;S0ubF-Cft_pT630m`~s3-R&KR$&bsnRefpxabF` ziJP}en|H#w-5V6nwMzO}3ynK;2HhPNS6~|ih9}0oO+&z3J^()fx}!tftcCpGclBIZ zi-MFQGm5<|$o*b{Segkwb`|<*YjwV*IiCM%6WWgbKGnAfzpVU-O&Cy!5_hcR%+f3Z z(hhbg!sm9evmyq$u|MEs4MBV@few;`dj`j%G^vu@oR4tkeXWsq-x^ir-his$6w=ip zET52`KOv3@#^V9c-eAM^P!-zM{!-ma~#lYK14e zk*={ZJ!q#BtTFm%SXuodG3A;lNfp1SxU+|U-bHg%A}ds9N3FUP7HFlDA4CrEZV;Iy z8<`Bjp#j`WQGQ@VMT=-^(hH8Y&0?ce;yV{URzUNh5i$ZfGy!89q6m;KS^rhTW0%qeQs#G8nCaDeS%4D5gg zUN&fJ(BdT2ltr41rvCIs^A}~Q3cFP4r}^d($e*uQQ5RXZGSzx9Fd7WKtUOEz4=tiv zqYC)n=UpCBJ;_5sg(uvBy`p2pXx(C<%Q-75E*cd4@FW_ZX~EfYV4=2+#`lyO>1D!` zz=ZeGiD~E!DhByJyfney%O( zS`KyDTJPj$`f_``wXKp+MXm8UE(EcTm2KOT)DeFskYgfMh->~L0kX+*dNqf1IH$(i zXnsCq`dUvkQR$~^=as)_KNuAIg2w^RLd|rN)Rda)Ff`DRg`2))H^&*na02Ht^>iR? zzV?$1o~619fZJ}i%^1Ass}r7C3$P+D%77^O&XLBn>xqSQNNSk-r!rHGxvIL`vwBX3 zINystWi8oy?3|m|#{(SWC)jnR+D^WYZbCypkQW`$a6{aD1JV2=y<(O)e%sNxykSva zL^>>Zm}1?VW^H}{_7%YdlfzAsUaxUmkL}yL`ott>+2)Fm8p$Pm!ztIzf3xMWY!r^6 z7jCk>gpdC|Dv~=p00ovv1pzvtX!#~q2>iGAyp7fpgxFs9%j?B&HY5Z7In@3%!2S^_ zjDtj_Uf=$NDpR%dH78!DtmLz+Ostef=<_BM%X9x)IO>2Oa5;VgCY%C^@8>Muu$Aux zjbHRTKallZ{==~CKH&!#)IBgjo7ERjh0yMagK4Pqg&%|Yj|@P>#|yTI5I%@RwQjqr zJ{!=}vkM7X{$d$3JkO5=0^@pd3d8A+7r*tq9J5vFhmpG5l=5%@g!qj-3U;6yNUpgV z$i;x9ZT89cV!Xm0U*0dFH^K2hgLvKXxci@)5IklnYn6&vxotlB&kmabD)Uc8?a@JEL!oXwQ*;l9|Znw>S>A`B8DF?5PZGXn@nN;e`Q$ViEFcXvq$NJ}Hg&*?s_K|?-pXzi7UW)V zgPqRNX4~~Owp=%$kdnsm=fm~1ooc(NtK_&B{@;}qxSaaL%d={R=Qo!1>DSWUK2=d* z`$8sF*08qmX=sSZ^uwHh;C~%g2r-+=`qQ~hqfWz?GHdG5%OEt~C+qy;3`bhAwW>|s zMF!2aq1l_jQl+J24v*~xi0PsnQ3tF?RpfY1$CUGnBbMysz`KK>q(b06O_0jXy+vBhkIYXsl-~{1N{$!EWiYhaQG2;EhLtYUINP9` zF|nDEet`dEnK&?;S=VAP;qz{?I2Vk`cHJ>LZyQUVtJak z)cI=hc|Zm6@x$T`oUL)0HTuW=MVaZv5$_f{`~B4{!>#g26cn5ZXyuL>$mZqaSnamI zA+t?V3sCL$zQdv$pPO$N0w!Kb42UlCNNB zulood0G4$uM{0c)o(&{f4=4!o6Uk&BOVE3~ zDbmO4;MjinOhIExbnlQto(!onTYIjw4er0^?%0%`MLjba*~={%bFI3&EaG$EwUyV) zQzSmOLOxq^;JF?&D$U?WIvCM@-zSmTdUvBTe%mjLYy@taWc2iyZ6-qO%+}S+_$zsY zk7TUZxE5IzUBcBi|3Ks;7J^!Nc;KDG^1zvdC52GNW!k}(L!3lN)q{i3N;r_tvA(B(Q|RIX}C*PZ?iOE z*?@z&!;f_?A%4?wmX>OP{j6-|ud00Ehj#01!)rsGCeMp*XXJh9TW0I8+u;$@jSRJ4 zdNXHFtvkHYIhUi?n>Q}Ee_qjpAsTy^mwK z$9Ut|dzW<@XSKuj!5|@j5i}f{#u+5(LbWNP>ee%B%)iQc{+7o{<}5i<7mEoy)I5sc zpGrN0^elx6{ZnurHl=W2rS3HCqj4KZs)17yB!0Q1`=zvv^w$QtJYcWAqN!q106{tt zuJxpwejB|`-eUy$Gec`HL}kDMduz@wmw;Q=@`al!w2Plg#)AK6OyFoGWut+l^;oZv z;O$-@q+or$EiYj>GCiF!1r3ZgM}nIkt0KV<-+MWrYIra|2}3@(seb)FxN6LMpyq#k zWL^$^7W#+CvAOGZA{8XqaIor2T-tvU$`Y3ha6<+B(d4@3rjT$l4?Qy^E#}6g z`z7_Stc~z6k1zI|jFBt;nf{T@ad2?n$Xk0)*Y|@adgDl&q#U!8dQD-O?1=@Er(UKd z&*mFP&fVB1MTbGo(hXmuK8V_lnJhmToJ#p4ZL4>^Lp);ee8HvO@NtWYF+sNd5X%9c zU<$nXRvz84OLHOMMaGqMR3!kxf{`B62{tlZjHJK3=ZB8;@Y!z>U({xQVf<#jCWuLJ z5d<*_^X#{UF-E+ov7KW^MI+S@`3`R>Y)3KK}CsV7?=#RiY(jvqulMZ ze)|$v9*R3!l6I<~oV|7}d)Ft@X{Cpxa*PN}uH?^yO#LyeleF9|jn_Hsxxhkl(*|VDYg{XIvyh1w4suke}Pb5zY(r1m+*Bb zMc1;GAX|6lUoFzxbRt*1XOs@k>Pm#;;R1l1+FK5fuw^Ag^pB3slHb5;57d5vq<4A1 zb~N5sp-DXH?cq&7w;twdJ|cDd35m9Etb>Zb+$djbOv-t~ZbD;1&NcM5OB*!{^X&(u3LPaSj_`VUsv?TzyZaq16}ACy z?auC4o+nZ=3|h*r&J(!IjR8Iq#@qwE?7WgIJW^qx=R&EEevCSEJb5P182a0f#shs) z!*md&ug7dn?#V;N0F~yGB>j2yJ)#$&A9GLB-Z?!oNiXQ6%*bL;0Q_L7zl`q_NG0e^ z$iZuJz!CXBm$wZRtH`l)s+!u9aKb%X3c+9cJ`(=TV+Dwrs%WYjD*gRg0zvS3l;UQD zxHu-I@nWK(GV!+QZ)7ahiPC&T1G!^^yr8~CpA(A5_r3@>5r!ODYPH!YPT+z@eL^0D zI~a5|WP*LB`IO#zBEfZhzJoOOLsb9YwjHDq5T?P{T7Uf*dv~U1rZqfDx$6j-8?kIS z#Wzp6frO}j&nfXng1d`*?}SBeh6b``Tmf}G7NLteDcW8wh@-pTz5iCj2P)}vmWpVG zl*NvERf&S#Pj>X!V^!c#!(4j5o8Pn)oL}C(t zc`sKRP#zC5wq$U42I)wHglcGLLapnuvsuJ+iD#~Ix3k2pBwqZ4 zXi`ifWM9bQmc0NAIBBmVlC%FPJ!%8C1k;FsF{HHmxdgL!iI4E9W3$A!B;+{-9q$$f z$?M66sFp)fF1~7+7$51a^2S={ovUZ{2!E6F`Z-O@TYFNxugMs9%ahr1#7a=or<*mOR z%zOT0r_t#>Ykcw_f{fd==El)pj_q^EPtbp}Xn7=$A8IGME2X;~!`4)JZFW1xPfXC{ z7NGJ^yEpJ9Ash123_QAraRoh?a2ZVYc0kpB=c4bXKgsgn%T||qsWGpjh!3+ z(9iKk*5o_y-Fiann4v6|!IojrlJxq0%o4+V_dxkY;DT^Mj8kwJDOWQUhiBg`|7cnQ zbziO;Rzf*2VGAfw3hMkCouVI0E^b~pB*<y zh$5OJ4veyAe6R{2XL;ewufCs9YnIK=RGT0@kIgs^IyTLMz3x`#=^>zZcK=uDB7QdR zy8W{pe7i`N>AZIcWMfxmcn=SWkGwDyKSr4W?kh@$nM4Uf$#3@M4@wq{b$uvvFo*hXu z5-lHTl-!ms^%zOueXe12t6xErDCd@*>G$;= zP7mla7$CQuxw6#9c!vRL_2G47G>F9Y!oh(2>dUnO0I@W_{7Fo!sESu`Wx5FsQhUtv z0AeNff=y^)2VRGuevjMZl{cPuEPAh(fW+hj`K?nVVRpazl=jCyr^DNPov$&K(u)TV zegIyeh`31IdSl{(Bgh$P&b8r2GpZElz$xP-`D5XhAgV}Q#Sr%?bpeI7afxB|y zrjU}($q`461;6?bCMfEaQk%?iR&`$9$(i?UI(LUEWo%55FpEhPmIbpH!M@?8@v0J@ zQB_|+ajz5bnEUfVh#_MuHkuw^cS@q_mQw#`9m45*3>^loM9T#fJjdy(eH#z=#7?kQ zoa*;Y_0O+*Yd?l3yl)QIA{(!z=cT1q>|} z`=y6Z2|l;yMU~4wpqu;Goiz_63zOHz@U)ipJ;J!z?}w?mgy5#1k;l#ham^bvVm--( zUdmid?D1+GZfy^0&Ik#MBt%JeHreq>Szd-}FJYN-t%+D^$vBtoxVF0WXJV^3;y4aV*sa|({5{n5Kvvo+GEn_h`88#-zkYQmmB+MW=9I zCp=FTA`Q-WY#I$>(4X>iV43!Az)}7_RYj(?N>QHO8PXH|)pWhfEX>OWVLGgnl=H)i z8Srd*OkAL4FL-^kz$9`v1U5$_buHvee<8PAB$>7>ZT5|+`f$1W>lX;DS{a-4W3#YS z8+5Ck5^U!eKBShg`K1IKGo!eUOX`JHsQ#F~mKEIA6VB%VvVzMwDB@iPC{(#Pf2fC)biFI7T*pWDgce(Sf!W1pm{UFQ8l z50HBw!;vgXX^)1T?%b?-+NjKT_aBRCd6N8L1!F4s+)%@NnvXwTM+&<1$a@~}@wno( zjNxN|LGG#)sYXhO)>yVa;rc-5f7aqt@frtHo}0auo;F#xr#wiSJ-_A{VHb<47r=Q& zK&G)!8mM0F)eYK`DpYY$AQJ!#!?%8Y$=(s*oHPKeYG+3TXJJuW_5mob}9c9Eii5zNJa5Wn*?IbSGA*oQRJbq)5UAJJ(ReT85B%{K(z^ zZa5SE_AM67lzL==%LyG8V~NZ1fyB{R6YfB(AD02pO8S{t!eMq+D}<_-`c!fR2c0$k4EqR`1(Ev*p#GTTy-X z(Y$Y@)n^`&l9tOs|7X8cu6ybi{;(PMB_flaCDI|%_x>fkgzj7cW1TfCWlKcc0 zG+Y0vx{KP|1JH3oNL+p?ab}X*jzT(;5IQAcv71(#*%L3&MrqgmaB9)a;KM}{IzH!&tcQ4ApeO{&QG@V2cLoNp*6a)#lp zFqXB@Z+96(kmE^KAELQZenmai^4cORD4amc_`DOtv%h|%*DOsiOyL?FCb-XcyBdZNBE{cUc+MK5kbAtV;Pg|j#7E9VECI;SAV`SDPq!843-twPJKrA$w zd3DpTUj>O&?>Fos2lOE5lV`5G=@K5}*Y}?Nh&FcP*meKVS1eD?K=$~;WK6$iY9A0p zd#12+dwH*RQ{p}bI2>Rmgj;X{Owhs0PNoUMS8qWRe8{ratScuvoC6uSZO3)DN> za_Rl&(i|2b6mT%1KAhvu&~&GXjS*YiB*rg~r9DD~x*AIeBYi|>TLFiNCkACKPdgaw zHsh*(2tjr@e9s%M3|VSi^&6ilL3_pAvtcWK-WkqeWnUBeZ(%m@n7kaR(S}v&d?rdp z7AMFBcqp}(@rbYh9W<pjnp4?sfN%6EF^)u=h zcfHj;ypgbw_J>B+EI?ij*Hrc=hSDJ}%Tn)ILtZQ5k0-HfJlq47=XN$b=4RXSy@%`C zn_O7o4bTAxjOr3yMJS2ul0f}t=5?nugVlHu)7`e*5E>G`-S-}foA(P#e!d>Qj*v&T z+V9`E{7&?fanVTOf}hu;JqLU;VN`dRzCS74vkRuDkSSO3j>+A-#96vm#+NzPwtcFr zk+8?K&~1iy-@mC`8~4Oq?sI8|mNH26sLX(E9*coI%Icuvz`h!;EnA$WZH-D}i+pdF zkU&}f-Hg&v1)fYZvmYQOG|A@52{G@yjoJ=PvBkwd3KqkLjsJS;j@>AyQp%*0v;{ni z*W+kOra6RC48F&GQ3IkKBHRT8J}bM~+M(br3K#G0NXo|a%-$3Q;r<19Rmq7y$BKDz zxh}M?ZQX}MzKC$zP~%B@TaF#lqud}Z(;~#VukZ}*MA*dkN*Uz!y_Sj<&$5UK|3<@{ z18xpxMHDGHHJDzWntaRG^`xh5TJuOl}y}W1QJH`869~qkSc1V)(-I3Y+PeQk&P_4@hhd z7lm!a*HWCIz%%;x)cYTo@iA>4{OZS0KFIv>mQ^MJD5%?jz0W6VZx~EMq`;7qZewD&OyVF zS7B6jCI92r%-P4^ZB-B&6-n}U|Ma(0`lrg`E0A5Q`J{xmkPT!V0*_u5A9>H^I6G?>EyvgL9{F~4l&zEgcHNlrT zUp|k%^JRsZq7ZxVwjk+K0@2?PH9yIo=O8TC^l{WY|9>(L+RKKPQDGKZ`dxxA*p+(f(-F;>K6F&xMk6K5qoT zYN!)7^UeF*5A{5*01}e_C@JPv1d?-9GQT$$tb1d;ZOj(;{2ylEqWm9^#sm9=XOm;} zupjDUM=T!>L(@reb#H7A8AU=;8+|?(upDmU`;?B#AQVyI{WS@7Rpj|t{#mradnd2& zlH1;K-y{~0YsI8w;dhB`7f*6a^|pVY39?=loK(6n3E76Zcf_R?jt5$e{;pF7iOCi6 zZsVpSE6~);0>@cZzgvr%Zl)&_;AXAJ-bBQhbK=dH@i?IapBjsM+@H*aySW*zNMvq) zNXzV-a?B$kuIGM=mc$Tz?3Q-4UO$s9+?43mK#d2;*gK~Y-i}%_jLlF76DT{T!B!;7 zeWu5LpvNWRy!HxwXk$*b3@DDC1U=!B@SFB&XtPOE?d8nfEZGLWEBh?#cEnq|&W&j_ z5yazK&=y6siH;P1T}09L8ZCoYpVw^M$QJDX5UwkBOS^g^Y3-*|?ZbXdhv#hesWbdR zLlpEuOdOS~8<8x2gzfaJF23bf*IgsX%-G!2y3@5;wT!)|gWG6W%o48d9AYaVw?6zT zm8=!{#xef<`fRnJ^r7429bdMR7$mgc!9{ANMPPYn0Y7G$%mqdJa9fkp4`Q zi+NT3G^t@5UP2n{RoP*bV1ZiGOX~DP`fPQ`#?JDU8n=yCD&4kX`E|D8JWf zCMFk`HFicK-XOc(NAT`m|KEXA5{ZVccgKAMc+d^2f0~T{`QM~i*v5eY9$md7ACq71 zmnJW3getPixelsPadding().Left + - mContainer->getPixelsPadding().Right; + mMinIntrinsicWidth = tmpRt.getMinIntrinsicWidth() + + mContainer->getPixelsContentOffset().Left + + mContainer->getPixelsContentOffset().Right; UIRichText::rebuildRichText( widget, tmpRt, UIRichText::IntrinsicMode::Max ); - mMaxIntrinsicWidth = tmpRt.getMaxIntrinsicWidth() + mContainer->getPixelsPadding().Left + - mContainer->getPixelsPadding().Right; + mMaxIntrinsicWidth = tmpRt.getMaxIntrinsicWidth() + + mContainer->getPixelsContentOffset().Left + + mContainer->getPixelsContentOffset().Right; mIntrinsicWidthsDirty = false; } } @@ -73,8 +75,8 @@ void BlockLayouter::updateLayout() { Float totW = mContainer->getPixelsSize().getWidth(); if ( mContainer->getLayoutWidthPolicy() == SizePolicy::WrapContent ) { - totW = rt->getSize().getWidth() + mContainer->getPixelsPadding().Left + - mContainer->getPixelsPadding().Right; + totW = rt->getSize().getWidth() + mContainer->getPixelsContentOffset().Left + + mContainer->getPixelsContentOffset().Right; if ( !mContainer->getMaxWidthEq().empty() && totW > mContainer->getMaxSizePx().getWidth() ) mContainer->setClipType( ClipType::ContentBox ); } @@ -85,8 +87,8 @@ void BlockLayouter::updateLayout() { Float totH = mContainer->getPixelsSize().getHeight(); if ( mContainer->getLayoutHeightPolicy() == SizePolicy::WrapContent ) { - totH = rt->getSize().getHeight() + mContainer->getPixelsPadding().Top + - mContainer->getPixelsPadding().Bottom; + totH = rt->getSize().getHeight() + mContainer->getPixelsContentOffset().Top + + mContainer->getPixelsContentOffset().Bottom; if ( !mContainer->getMaxHeightEq().empty() && totH > mContainer->getMaxSizePx().getHeight() ) mContainer->setClipType( ClipType::ContentBox ); @@ -158,12 +160,12 @@ void BlockLayouter::positionRichTextChildren( Graphics::RichText* rt ) { bool passedText = false; for ( const auto& rspan : line.spans ) { if ( rspan.startCharIndex >= startChar && rspan.endCharIndex <= endChar ) { - Rectf hb( mContainer->getPixelsPadding().Left + rspan.position.x, - mContainer->getPixelsPadding().Top + line.y + + Rectf hb( mContainer->getPixelsContentOffset().Left + rspan.position.x, + mContainer->getPixelsContentOffset().Top + line.y + rspan.position.y, - mContainer->getPixelsPadding().Left + rspan.position.x + + mContainer->getPixelsContentOffset().Left + rspan.position.x + rspan.size.getWidth(), - mContainer->getPixelsPadding().Top + line.y + + mContainer->getPixelsContentOffset().Top + line.y + rspan.position.y + rspan.size.getHeight() ); hitBoxes.push_back( hb ); @@ -212,8 +214,8 @@ void BlockLayouter::positionRichTextChildren( Graphics::RichText* rt ) { } widget->setPixelsPosition( pos ); widget->setPixelsSize( { eemax( 0.f, mContainer->getPixelsSize().getWidth() - - mContainer->getPixelsPadding().Left - - mContainer->getPixelsPadding().Right ), + mContainer->getPixelsContentOffset().Left - + mContainer->getPixelsContentOffset().Right ), 0 } ); } else { curCharIdx += 1; @@ -223,9 +225,10 @@ void BlockLayouter::positionRichTextChildren( Graphics::RichText* rt ) { Float lineY = lines[lineIdx].y; Rectf margin = widget->getLayoutPixelsMargin(); - Vector2f targetPos( - mContainer->getPixelsPadding().Left + span->position.x + margin.Left, - mContainer->getPixelsPadding().Top + lineY + span->position.y + margin.Top ); + Vector2f targetPos( mContainer->getPixelsContentOffset().Left + span->position.x + + margin.Left, + mContainer->getPixelsContentOffset().Top + lineY + + span->position.y + margin.Top ); widget->setPixelsPosition( targetPos - offset ); diff --git a/src/eepp/ui/tablelayouter.cpp b/src/eepp/ui/tablelayouter.cpp index 763896c33..f5322bf42 100644 --- a/src/eepp/ui/tablelayouter.cpp +++ b/src/eepp/ui/tablelayouter.cpp @@ -89,7 +89,7 @@ void TableLayouter::computeIntrinsicWidths() { if ( mRows.empty() ) { mMinIntrinsicWidth = mMaxIntrinsicWidth = - mContainer->getPixelsPadding().Left + mContainer->getPixelsPadding().Right; + mContainer->getPixelsContentOffset().Left + mContainer->getPixelsContentOffset().Right; mIntrinsicWidthsDirty = false; return; } @@ -114,7 +114,7 @@ void TableLayouter::computeIntrinsicWidths() { if ( maxCols == 0 ) { mMinIntrinsicWidth = mMaxIntrinsicWidth = - mContainer->getPixelsPadding().Left + mContainer->getPixelsPadding().Right; + mContainer->getPixelsContentOffset().Left + mContainer->getPixelsContentOffset().Right; mIntrinsicWidthsDirty = false; return; } @@ -271,10 +271,10 @@ void TableLayouter::computeIntrinsicWidths() { totalMax += mColMaxWidths[i]; } - mMinIntrinsicWidth = totalMin + mContainer->getPixelsPadding().Left + - mContainer->getPixelsPadding().Right + ( maxCols + 1 ) * mCellspacing; - mMaxIntrinsicWidth = totalMax + mContainer->getPixelsPadding().Left + - mContainer->getPixelsPadding().Right + ( maxCols + 1 ) * mCellspacing; + mMinIntrinsicWidth = totalMin + mContainer->getPixelsContentOffset().Left + + mContainer->getPixelsContentOffset().Right + ( maxCols + 1 ) * mCellspacing; + mMaxIntrinsicWidth = totalMax + mContainer->getPixelsContentOffset().Left + + mContainer->getPixelsContentOffset().Right + ( maxCols + 1 ) * mCellspacing; mIntrinsicWidthsDirty = false; } @@ -306,7 +306,7 @@ void TableLayouter::updateLayout() { size_t maxCols = mColMinWidths.size(); mColWidths.assign( maxCols, 0.f ); - Float paddingH = mContainer->getPixelsPadding().Left + mContainer->getPixelsPadding().Right; + Float paddingH = mContainer->getPixelsContentOffset().Left + mContainer->getPixelsContentOffset().Right; Float containerWidth = mContainer->getPixelsSize().getWidth(); Float availableWidth = sanitizeFloat( std::max( 0.f, containerWidth - paddingH - ( maxCols + 1 ) * mCellspacing ) ); @@ -510,23 +510,23 @@ void TableLayouter::updateLayout() { mFooter->setPixelsSize( { mContainer->getPixelsSize().x, footerHeight } ); } - Float currentY = mContainer->getPixelsPadding().Top + mCellspacing - headHeight; + Float currentY = mContainer->getPixelsContentOffset().Top + mCellspacing - headHeight; for ( size_t r = 0; r < rowCount; ++r ) { UIHTMLTableRow* row = mRows[r]; - row->setPixelsPosition( mContainer->getPixelsPadding().Left, currentY ); + row->setPixelsPosition( mContainer->getPixelsContentOffset().Left, currentY ); currentY += row->getPixelsSize().getHeight() + mCellspacing; } if ( mHead && !mRows.empty() ) - mRows[0]->setPixelsPosition( mContainer->getPixelsPadding().Left, 0 ); + mRows[0]->setPixelsPosition( mContainer->getPixelsContentOffset().Left, 0 ); if ( mFooter && !mRows.empty() ) - mRows[rowCount - 1]->setPixelsPosition( mContainer->getPixelsPadding().Left, 0 ); + mRows[rowCount - 1]->setPixelsPosition( mContainer->getPixelsContentOffset().Left, 0 ); if ( mContainer->getLayoutHeightPolicy() == SizePolicy::WrapContent ) { mContainer->asType()->setInternalPixelsHeight( - mContainer->getPixelsPadding().Top + headHeight + bodyHeight + footerHeight + - ( rowCount + 1 ) * mCellspacing + mContainer->getPixelsPadding().Bottom ); + mContainer->getPixelsContentOffset().Top + headHeight + bodyHeight + footerHeight + + ( rowCount + 1 ) * mCellspacing + mContainer->getPixelsContentOffset().Bottom ); } mPacking = false; diff --git a/src/eepp/ui/uiborderdrawable.cpp b/src/eepp/ui/uiborderdrawable.cpp index 1774bf97d..bf6e26621 100644 --- a/src/eepp/ui/uiborderdrawable.cpp +++ b/src/eepp/ui/uiborderdrawable.cpp @@ -225,6 +225,8 @@ void UIBorderDrawable::setBottomRightRadius( const std::string& radius ) { } const Borders& UIBorderDrawable::getBorders() const { + if ( mNeedsUpdate ) + updateBorders(); return mBorders; } @@ -249,6 +251,9 @@ void UIBorderDrawable::onPositionChange() { } Rectf UIBorderDrawable::getBorderBoxDiff() const { + if ( mNeedsUpdate ) + updateBorders(); + Rectf bd; switch ( mBorderType ) { case BorderType::Outside: { @@ -342,7 +347,7 @@ void UIBorderDrawable::update() { mNeedsUpdate = false; } -void UIBorderDrawable::updateBorders() { +void UIBorderDrawable::updateBorders() const { if ( !mBorderStr.width.left.empty() ) { mBorders.left.width = mOwner->lengthFromValue( mBorderStr.width.left, CSS::PropertyRelativeTarget::LocalBlockRadiusWidth ); diff --git a/src/eepp/ui/uihtmlwidget.cpp b/src/eepp/ui/uihtmlwidget.cpp index 220d477c3..6e96404df 100644 --- a/src/eepp/ui/uihtmlwidget.cpp +++ b/src/eepp/ui/uihtmlwidget.cpp @@ -199,7 +199,7 @@ void UIHTMLWidget::positionOutOfFlowChildren() { Float top = PixelDensity::dpToPx( offsets.Top ); Float left = PixelDensity::dpToPx( offsets.Left ); - Vector2f cbPos( cb->getPixelsPadding().Left, cb->getPixelsPadding().Top ); + Vector2f cbPos( cb->getPixelsContentOffset().Left, cb->getPixelsContentOffset().Top ); cbPos.x += left; cbPos.y += top; diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index 711fac9e2..a7f471589 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -176,14 +176,16 @@ void UIRichText::draw() { UIWidget::draw(); if ( mRichText.getSize().getWidth() > 0.f ) { + Rectf contentOffset = getPixelsContentOffset(); if ( isClipped() ) { - clipSmartEnable( mScreenPos.x + mPaddingPx.Left, mScreenPos.y + mPaddingPx.Top, - mSize.getWidth() - mPaddingPx.Left - mPaddingPx.Right, - mSize.getHeight() - mPaddingPx.Top - mPaddingPx.Bottom ); + clipSmartEnable( mScreenPos.x + contentOffset.Left, + mScreenPos.y + contentOffset.Top, + mSize.getWidth() - contentOffset.Left - contentOffset.Right, + mSize.getHeight() - contentOffset.Top - contentOffset.Bottom ); } - mRichText.draw( std::trunc( mScreenPos.x ) + (int)mPaddingPx.Left, - std::trunc( mScreenPos.y ) + (int)mPaddingPx.Top, Vector2f::One, 0.f, + mRichText.draw( std::trunc( mScreenPos.x ) + (int)contentOffset.Left, + std::trunc( mScreenPos.y ) + (int)contentOffset.Top, Vector2f::One, 0.f, getBlendMode() ); if ( isClipped() ) @@ -586,15 +588,16 @@ void UIRichText::onAlphaChange() { void UIRichText::rebuildRichText( UILayout* container, RichText& richText, IntrinsicMode mode ) { richText.clear(); - Float maxWidth = container->getPixelsSize().getWidth() - container->getPixelsPadding().Left - - container->getPixelsPadding().Right; + Float maxWidth = container->getPixelsSize().getWidth() - + container->getPixelsContentOffset().Left - + container->getPixelsContentOffset().Right; if ( maxWidth < 0 ) maxWidth = 0; Float mw = 0.f; if ( !container->getMaxWidthEq().empty() ) { - mw = container->getMaxSizePx().getWidth() - container->getPixelsPadding().Left - - container->getPixelsPadding().Right; + mw = container->getMaxSizePx().getWidth() - container->getPixelsContentOffset().Left - + container->getPixelsContentOffset().Right; if ( mw < 0 ) mw = 0.f; } @@ -636,8 +639,8 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri if ( isBlock ) { if ( container->getPixelsSize().getWidth() != 0 ) { Float maxSize = eemax( 0.f, container->getPixelsSize().getWidth() - - container->getPixelsPadding().Left - - container->getPixelsPadding().Right - + container->getPixelsContentOffset().Left - + container->getPixelsContentOffset().Right - margin.Left - margin.Right ); widget->setPixelsSize( eemax( 0.f, maxSize ), widget->getPixelsSize().getHeight() ); @@ -663,8 +666,8 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri if ( isBlock && mode == IntrinsicMode::None && container->getPixelsSize().getWidth() != 0 ) { w = eemax( 0.f, container->getPixelsSize().getWidth() - - container->getPixelsPadding().Left - - container->getPixelsPadding().Right - margin.Left - + container->getPixelsContentOffset().Left - + container->getPixelsContentOffset().Right - margin.Left - margin.Right ); } @@ -722,10 +725,12 @@ Float UIRichText::getMinIntrinsicWidth() const { RichText richText( mRichText ); UIRichText::rebuildRichText( const_cast( this ), richText, IntrinsicMode::Min ); - mMinIntrinsicWidth = richText.getMinIntrinsicWidth() + mPaddingPx.Left + mPaddingPx.Right; + mMinIntrinsicWidth = richText.getMinIntrinsicWidth() + getPixelsContentOffset().Left + + getPixelsContentOffset().Right; UIRichText::rebuildRichText( const_cast( this ), richText, IntrinsicMode::Max ); - mMaxIntrinsicWidth = richText.getMaxIntrinsicWidth() + mPaddingPx.Left + mPaddingPx.Right; + mMaxIntrinsicWidth = richText.getMaxIntrinsicWidth() + getPixelsContentOffset().Left + + getPixelsContentOffset().Right; mIntrinsicWidthsDirty = false; } @@ -749,11 +754,11 @@ Float UIRichText::getMaxIntrinsicWidth() const { if ( mIntrinsicWidthsDirty ) { RichText richText( mRichText ); const_cast( this )->rebuildRichText( richText, IntrinsicMode::Min ); - mMinIntrinsicWidth = - richText.getMinIntrinsicWidth() + mPaddingPx.Left + mPaddingPx.Right; + mMinIntrinsicWidth = richText.getMinIntrinsicWidth() + getPixelsContentOffset().Left + + getPixelsContentOffset().Right; const_cast( this )->rebuildRichText( richText, IntrinsicMode::Max ); - mMaxIntrinsicWidth = - richText.getMaxIntrinsicWidth() + mPaddingPx.Left + mPaddingPx.Right; + mMaxIntrinsicWidth = richText.getMaxIntrinsicWidth() + getPixelsContentOffset().Left + + getPixelsContentOffset().Right; mIntrinsicWidthsDirty = false; } maxW = mMaxIntrinsicWidth; diff --git a/src/eepp/ui/uiwidget.cpp b/src/eepp/ui/uiwidget.cpp index a8719bdc6..41960f721 100644 --- a/src/eepp/ui/uiwidget.cpp +++ b/src/eepp/ui/uiwidget.cpp @@ -593,12 +593,12 @@ void UIWidget::calculateAutoMargin() { UIWidget* parent = getParent()->asType(); Sizef parentSize = parent->getPixelsSize(); - Rectf parentPadding = parent->getPixelsPadding(); + Rectf parentContentOffset = parent->getPixelsContentOffset(); bool changed = false; if ( ( mMarginAuto & MarginAutoLeft ) && ( mMarginAuto & MarginAutoRight ) ) { - Float availableWidth = parentSize.getWidth() - parentPadding.Left - parentPadding.Right - - getPixelsSize().getWidth(); + Float availableWidth = parentSize.getWidth() - parentContentOffset.Left - + parentContentOffset.Right - getPixelsSize().getWidth(); Float newMarginLeft = availableWidth > 0 ? availableWidth / 2.f : 0.f; Float newMarginRight = availableWidth > 0 ? availableWidth / 2.f : 0.f; if ( mLayoutMarginPx.Left != newMarginLeft || mLayoutMarginPx.Right != newMarginRight ) { @@ -607,16 +607,18 @@ void UIWidget::calculateAutoMargin() { changed = true; } } else if ( mMarginAuto & MarginAutoLeft ) { - Float availableWidth = parentSize.getWidth() - parentPadding.Left - parentPadding.Right - - getPixelsSize().getWidth() - mLayoutMarginPx.Right; + Float availableWidth = parentSize.getWidth() - parentContentOffset.Left - + parentContentOffset.Right - getPixelsSize().getWidth() - + mLayoutMarginPx.Right; Float newMarginLeft = std::max( 0.f, availableWidth ); if ( mLayoutMarginPx.Left != newMarginLeft ) { mLayoutMarginPx.Left = newMarginLeft; changed = true; } } else if ( mMarginAuto & MarginAutoRight ) { - Float availableWidth = parentSize.getWidth() - parentPadding.Left - parentPadding.Right - - getPixelsSize().getWidth() - mLayoutMarginPx.Left; + Float availableWidth = parentSize.getWidth() - parentContentOffset.Left - + parentContentOffset.Right - getPixelsSize().getWidth() - + mLayoutMarginPx.Left; Float newMarginRight = std::max( 0.f, availableWidth ); if ( mLayoutMarginPx.Right != newMarginRight ) { mLayoutMarginPx.Right = newMarginRight; @@ -625,8 +627,8 @@ void UIWidget::calculateAutoMargin() { } if ( ( mMarginAuto & MarginAutoTop ) && ( mMarginAuto & MarginAutoBottom ) ) { - Float availableHeight = parentSize.getHeight() - parentPadding.Top - parentPadding.Bottom - - getPixelsSize().getHeight(); + Float availableHeight = parentSize.getHeight() - parentContentOffset.Top - + parentContentOffset.Bottom - getPixelsSize().getHeight(); Float newMarginTop = availableHeight > 0 ? availableHeight / 2.f : 0.f; Float newMarginBottom = availableHeight > 0 ? availableHeight / 2.f : 0.f; if ( mLayoutMarginPx.Top != newMarginTop || mLayoutMarginPx.Bottom != newMarginBottom ) { @@ -635,16 +637,18 @@ void UIWidget::calculateAutoMargin() { changed = true; } } else if ( mMarginAuto & MarginAutoTop ) { - Float availableHeight = parentSize.getHeight() - parentPadding.Top - parentPadding.Bottom - - getPixelsSize().getHeight() - mLayoutMarginPx.Bottom; + Float availableHeight = parentSize.getHeight() - parentContentOffset.Top - + parentContentOffset.Bottom - getPixelsSize().getHeight() - + mLayoutMarginPx.Bottom; Float newMarginTop = std::max( 0.f, availableHeight ); if ( mLayoutMarginPx.Top != newMarginTop ) { mLayoutMarginPx.Top = newMarginTop; changed = true; } } else if ( mMarginAuto & MarginAutoBottom ) { - Float availableHeight = parentSize.getHeight() - parentPadding.Top - parentPadding.Bottom - - getPixelsSize().getHeight() - mLayoutMarginPx.Top; + Float availableHeight = parentSize.getHeight() - parentContentOffset.Top - + parentContentOffset.Bottom - getPixelsSize().getHeight() - + mLayoutMarginPx.Top; Float newMarginBottom = std::max( 0.f, availableHeight ); if ( mLayoutMarginPx.Bottom != newMarginBottom ) { mLayoutMarginPx.Bottom = newMarginBottom; @@ -837,6 +841,18 @@ const Rectf& UIWidget::getPixelsPadding() const { return mPaddingPx; } +Rectf UIWidget::getPixelsContentOffset() const { + Rectf offset = getPixelsPadding(); + if ( hasBorder() ) { + const auto& b = getBorder()->getBorders(); + offset.Left += b.left.width; + offset.Right += b.right.width; + offset.Top += b.top.width; + offset.Bottom += b.bottom.width; + } + return offset; +} + UIWidget* UIWidget::setPadding( const Rectf& padding ) { if ( padding != mPadding ) { mPadding = padding; @@ -2578,7 +2594,7 @@ Float UIWidget::getMatchParentWidth() const { Rectf padding = Rectf::Zero; if ( getParent()->isWidget() ) - padding = static_cast( getParent() )->getPixelsPadding(); + padding = static_cast( getParent() )->getPixelsContentOffset(); Float marginLeft = ( mMarginAuto & MarginAutoLeft ) ? 0.f : mLayoutMarginPx.Left; Float marginRight = ( mMarginAuto & MarginAutoRight ) ? 0.f : mLayoutMarginPx.Right; @@ -2599,7 +2615,7 @@ Float UIWidget::getMatchParentHeight() const { Rectf padding = Rectf::Zero; if ( getParent()->isWidget() ) - padding = static_cast( getParent() )->getPixelsPadding(); + padding = static_cast( getParent() )->getPixelsContentOffset(); Float marginTop = ( mMarginAuto & MarginAutoTop ) ? 0.f : mLayoutMarginPx.Top; Float marginBottom = ( mMarginAuto & MarginAutoBottom ) ? 0.f : mLayoutMarginPx.Bottom;