mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-05-28 17:16:29 +03:00
CSS Position (Out of Flow) implementation.
Added a few plan descriptions that I want to implement later.
This commit is contained in:
61
.agent/plans/layout_separation_float_plan.md
Normal file
61
.agent/plans/layout_separation_float_plan.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# UI Layout Phase 6: CSS Float & Clear Plan
|
||||
|
||||
This document outlines the architectural plan for implementing CSS `float` and `clear` support within the decoupled layout system, leveraging the `Graphics::RichText` engine for mixed content formatting.
|
||||
|
||||
**AGENT DIRECTIVE (CRITICAL):** You MUST compile and run the unit tests (`bin/unit_tests/eepp-unit_tests-debug`) after EVERY step. Do NOT proceed to the next step if there is even a 1-pixel difference in visual layout tests. Take a git stash snapshot (`git stash push -m "Phase 6.X passed" && git stash apply`) upon passing a step to keep a checkpoint while continuing to work. **If you need to restore a stash, use `git stash apply` instead of `git stash pop` so the stable snapshot is never lost.**
|
||||
|
||||
---
|
||||
|
||||
## IMPLEMENTATION HAZARDS (READ BEFORE CODING)
|
||||
1. **Keyword Collision:** `Float` is a C++ type (`typedef float Float`). When defining the CSS enum, you MUST name it `CSSFloat` to avoid compiler collisions.
|
||||
2. **Y-Coordinate Interleaving:** `RichText::updateLayout` currently breaks lines independently of their Y position, and only computes Y coordinates *after* all lines are formed. Because floating elements alter the available horizontal width at specific Y coordinate ranges, you will have to calculate `curY` *during* the block iteration, keeping track of active floats to restrict `curX` and `maxWidth`.
|
||||
3. **Out-Of-Flow Precedence:** Floating elements are *not* out-of-flow in the same way `position: absolute` elements are. `absolute` elements are ignored by layouters, whereas `float` elements strictly participate in and influence the block formatting context (they take up space and push text around). Do not mark them as `isOutOfFlow() = true` in `UIRichText::rebuildRichText`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Float and Clear implementation
|
||||
|
||||
**Step 6.1: CSS Enums and Properties**
|
||||
- In `csslayouttypes.hpp`, define:
|
||||
```cpp
|
||||
enum class CSSFloat { None, Left, Right };
|
||||
enum class CSSClear { None, Left, Right, Both };
|
||||
```
|
||||
And their helper parsing functions (`CSSFloatHelper::fromString`, etc.).
|
||||
- In `propertydefinition.hpp`, ensure `PropertyId::Float` and `PropertyId::Clear` exist (if not, add them, avoiding conflicts).
|
||||
- In `UIHTMLWidget`, add `mFloat` and `mClear` members (defaulting to `None`).
|
||||
- In `UIHTMLWidget::applyProperty`, parse the `Float` and `Clear` properties. Call `notifyLayoutAttrChange()` when they change.
|
||||
- **Validation:** Compile and run all tests. Must pass. (Snapshot)
|
||||
|
||||
**Step 6.2: Extend RichText API**
|
||||
- In `include/eepp/graphics/richtext.hpp`, update `RichText::addCustomSize`:
|
||||
```cpp
|
||||
void addCustomSize( const Sizef& size, bool isBlock, CSSFloat floatType = CSSFloat::None, CSSClear clearType = CSSClear::None );
|
||||
```
|
||||
- Update `CustomBlock` struct to store `floatType` and `clearType`.
|
||||
- In `UIRichText::rebuildRichText`, extract `getCSSFloat()` and `getCSSClear()` from the child widget (defaulting to `None` if the child isn't an HTML widget). Pass these to `richText.addCustomSize`.
|
||||
- **Validation:** Compile and run all tests. (Snapshot)
|
||||
|
||||
**Step 6.3: Core RichText Layout Algorithm (The Tricky Part)**
|
||||
- In `RichText::updateLayout()`, introduce Y-coordinate awareness during the main loop:
|
||||
- Create tracking lists: `std::vector<Rectf> leftFloats; std::vector<Rectf> rightFloats;`
|
||||
- Introduce `Float curY = 0;`
|
||||
- Before placing *any* block (text or custom), process `clear`: if the block has `clear: left`, advance `curY` past the `bottom` of all `leftFloats`. (Same for `right` and `both`). Reset `curX` and push a new `RenderParagraph` if `curY` changed.
|
||||
- Compute `availableLeft(curY)` and `availableRight(curY, mMaxWidth)`. Your `curX` must never be less than `availableLeft`.
|
||||
- **If the block is a float:**
|
||||
- Place it immediately at `availableLeft` (if left) or `availableRight - width` (if right).
|
||||
- Add its bounding box `{x, curY, width, height}` to the respective float list.
|
||||
- Do *not* advance `curX` for the normal inline flow.
|
||||
- Do *not* trigger a new line for normal flow text (floats are pulled out of the inline line box).
|
||||
- **If the block is normal text/inline:**
|
||||
- Adjust `LineWrap::computeLineBreaksEx` to respect the narrowed `mMaxWidth` computed from `availableRight - availableLeft`. *(Note: You may need to handle the case where text wraps below a float and reclaims full width. This can be done by processing text in line-height chunks if constrained by a float).*
|
||||
- Make sure `curY` is updated when normal lines wrap.
|
||||
- **Validation:** This is the most complex step. Ensure all existing tests pass exactly (0 pixels difference) before writing float-specific tests. (Snapshot)
|
||||
|
||||
**Step 6.4: Float/Clear Layout Tests**
|
||||
- In `src/tests/unit_tests/uihtml_position_tests.cpp` (or a new `uihtml_float_tests.cpp`), write robust tests for:
|
||||
- Text wrapping around a `float: left` block.
|
||||
- Two consecutive `float: left` blocks stacking horizontally.
|
||||
- A block with `clear: both` jumping below all floats.
|
||||
- `BlockLayouter` correctly locating the `CustomBlock` widgets where `RichText` positioned the floats.
|
||||
- **Validation:** Compile and run all tests. Must pass. (Snapshot)
|
||||
55
.agent/plans/layout_separation_form_plan.md
Normal file
55
.agent/plans/layout_separation_form_plan.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# UI Layout Phase 8: Form Action and Navigation Plan
|
||||
|
||||
This document outlines the architectural plan for implementing HTML `<form>` submissions, input value extraction, and expanding `UISceneNode` to support interceptable navigation requests (GET/POST).
|
||||
|
||||
**AGENT DIRECTIVE (CRITICAL):** You MUST compile and run the unit tests (`bin/unit_tests/eepp-unit_tests-debug`) after EVERY step. Do NOT proceed to the next step if there is even a 1-pixel difference in visual layout tests. Take a git stash snapshot (`git stash push -m "Phase 8.X passed" && git stash apply`) upon passing a step to keep a checkpoint while continuing to work. **If you need to restore a stash, use `git stash apply` instead of `git stash pop` so the stable snapshot is never lost.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Form and Navigation implementation
|
||||
|
||||
**Step 8.1: Extend Navigation System**
|
||||
- The current `UISceneNode::openURL(URI)` and `setURLInterceptorCb` only handle simple URIs, which cannot represent `POST` requests or request bodies.
|
||||
- In `include/eepp/ui/uiscenenode.hpp`, create:
|
||||
```cpp
|
||||
struct NavigationRequest {
|
||||
URI uri;
|
||||
std::string method{ "GET" };
|
||||
std::string body;
|
||||
std::map<std::string, std::string> extraHeaders;
|
||||
};
|
||||
```
|
||||
- Add `void navigate( const NavigationRequest& request );` to `UISceneNode`.
|
||||
- Add `void setNavigationInterceptorCb( std::function<bool( const NavigationRequest& request )> cb );`.
|
||||
- Update `openURL(URI)` to wrap `navigate(NavigationRequest{uri})` for backward compatibility.
|
||||
- In `navigate()`, if `mNavigationInterceptorCb` returns `true`, return early. Else if `mURLInterceptorCb` returns `true`, return early. Else, fallback to `Engine::instance()->openURI()`.
|
||||
- **Validation:** Compile and run all tests. (Snapshot)
|
||||
|
||||
**Step 8.2: Retrieve Values from Form Elements**
|
||||
- Form submission requires querying the value of input elements.
|
||||
- Add `virtual String getValue() const { return String(); }` to `UIWidget`.
|
||||
- Override `getValue()` in the appropriate classes:
|
||||
- `HTMLInput`: return `getText()` for text, or `"on"`/`""` for checkboxes/radio buttons based on `isChecked()`.
|
||||
- `HTMLTextArea` (and `UITextEdit`): return `getText()`.
|
||||
- `UIDropDownList` (and `UIComboBox`): return the selected item's text.
|
||||
- Add `virtual String getName() const { return getAttribute("name"); }` or rely on `getAttribute("name")` to get the field identifier.
|
||||
- **Validation:** Write unit tests to verify `getValue()` for text, checkbox, and dropdowns. Compile and run all tests. (Snapshot)
|
||||
|
||||
**Step 8.3: Implement UIHTMLForm**
|
||||
- Create `UIHTMLForm` class inheriting from `UIHTMLWidget` (or `UIRichText` if treating as a block container).
|
||||
- Add members: `mAction` (URI), `mMethod` (String, default "GET"), `mEnctype` (String).
|
||||
- Override `applyProperty` to capture `action`, `method`, and `enctype`.
|
||||
- Implement `void submit()`.
|
||||
- `submit()` iterates over all child widgets recursively.
|
||||
- If a widget has a non-empty `name` attribute (using `getAttribute("name")`), it collects its `getValue()`.
|
||||
- It URL-encodes the keys and values.
|
||||
- If `mMethod == "GET"`, it appends the URL-encoded query string to `mAction` and calls `navigate()`.
|
||||
- If `mMethod == "POST"`, it puts the URL-encoded string into the `body` of `NavigationRequest`, sets `method = "POST"`, and calls `navigate()`.
|
||||
- In `uiwidgetcreator.cpp`, update `registeredWidget["form"]` to instantiate `UIHTMLForm::New`.
|
||||
- **Validation:** Compile and run all tests. (Snapshot)
|
||||
|
||||
**Step 8.4: Form Submission Triggers & Testing**
|
||||
- In `UIHTMLForm`, listen for `Event::OnMouseClick` on any child widget. If the target is a submit button (e.g., `HTMLInput` with `type="submit"`, or a `UIPushButton` with `type="submit"`), prevent the default action and call `submit()`.
|
||||
- Listen to `Event::OnPressEnter` inside text inputs within the form to trigger `submit()`.
|
||||
- Write a unit test simulating a form with inputs and a submit button. Attach a `NavigationInterceptorCb` to the scene node, simulate a click on the submit button, and verify the intercepted `NavigationRequest` contains the correct URI and encoded body.
|
||||
- **Validation:** Compile and run all tests. Must pass. (Snapshot)
|
||||
57
.agent/plans/layout_separation_list_style_plan.md
Normal file
57
.agent/plans/layout_separation_list_style_plan.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# UI Layout Phase 7: CSS List Style Type Plan
|
||||
|
||||
This document outlines the architectural plan for implementing the CSS `list-style-type` property within the decoupled layout system. This replaces the current CSS background-image hacks with proper text-based list markers.
|
||||
|
||||
**AGENT DIRECTIVE (CRITICAL):** You MUST compile and run the unit tests (`bin/unit_tests/eepp-unit_tests-debug`) after EVERY step. Do NOT proceed to the next step if there is even a 1-pixel difference in visual layout tests. Take a git stash snapshot (`git stash push -m "Phase 7.X passed" && git stash apply`) upon passing a step to keep a checkpoint while continuing to work. **If you need to restore a stash, use `git stash apply` instead of `git stash pop` so the stable snapshot is never lost.**
|
||||
|
||||
---
|
||||
|
||||
## IMPLEMENTATION HAZARDS (READ BEFORE CODING)
|
||||
1. **Property Inheritance:** `list-style-type` must be an inherited property so that setting it on `<ul>` or `<ol>` applies it to the `<li>` children.
|
||||
2. **Clipping:** `UIRichText` clips its content to the content box (`mSize - mPadding`). In CSS, when `list-style-position` is `outside` (the default), the marker is drawn in the padding/margin area. The marker MUST be drawn outside of the `clipSmartEnable` block in `UIRichText::draw()` to remain visible.
|
||||
3. **Sibling Indexing:** Ordered lists (`decimal`, etc.) require knowing the element's index. Only count previous siblings that are actual `<li>` elements (check `getTag() == "li"` or similar).
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: List Style Type implementation
|
||||
|
||||
**Step 7.1: CSS Enums and Properties**
|
||||
- In `csslayouttypes.hpp`, define:
|
||||
```cpp
|
||||
enum class CSSListStyleType { None, Disc, Circle, Square, Decimal, LowerAlpha, UpperAlpha, LowerRoman, UpperRoman };
|
||||
```
|
||||
And its helper functions (`CSSListStyleTypeHelper::fromString`, etc.).
|
||||
- In `propertydefinition.hpp`, add `PropertyId::ListStyleType`.
|
||||
- In `stylesheetspecification.cpp`, register the property as inherited:
|
||||
```cpp
|
||||
registerProperty( "list-style-type", "none", true );
|
||||
```
|
||||
- In `UIHTMLWidget`, add `mListStyleType` defaulting to `None`. Update `applyProperty` to parse it.
|
||||
- **Validation:** Compile and run all tests. Must pass. (Snapshot)
|
||||
|
||||
**Step 7.2: Marker String Generation & Text Cache**
|
||||
- In `include/eepp/ui/uirichtext.hpp`, add a `Text mListMarkerText;` member to cache the rendered marker.
|
||||
- Add a helper function `String getListMarkerString() const;` that:
|
||||
- Returns `""` for `None`.
|
||||
- Returns appropriate unicode characters for `Disc` ("•"), `Circle` ("○"), `Square` ("■").
|
||||
- For ordered types like `Decimal`, counts preceding `<li>` siblings and formats the string (e.g., `1. `, `2. `).
|
||||
- In `UIRichText::updateLayout()`, after styling is resolved, update `mListMarkerText`:
|
||||
- Set its string using `getListMarkerString()`.
|
||||
- Copy the font, size, and color from `mRichText.getFontStyleConfig()`.
|
||||
- **Validation:** Compile and run all tests. (Snapshot)
|
||||
|
||||
**Step 7.3: Rendering the Marker**
|
||||
- In `UIRichText::draw()`, add logic to render `mListMarkerText` if its string is not empty.
|
||||
- **Positioning:**
|
||||
- `X`: `mScreenPos.x + mPaddingPx.Left - mListMarkerText.getTextWidth() - offset`. (You may use a small hardcoded offset like `0.25em` derived from the font size to give it breathing room from the text).
|
||||
- `Y`: `mScreenPos.y + mPaddingPx.Top`. This aligns it with the start of the first line of text.
|
||||
- Ensure the drawing logic is placed *outside* the `if (isClipped()) { clipSmartEnable... }` block so it is not clipped away by the padding.
|
||||
- **Validation:** Compile and run all tests. (Snapshot)
|
||||
|
||||
**Step 7.4: Remove CSS Hacks & Update Tests**
|
||||
- Open `bin/assets/ui/breeze.css` and remove the `background-image`, `background-tint`, `background-position`, and `background-size` hacks for `ol > li` and `ul > li`.
|
||||
- Change them to properly use `list-style-type`:
|
||||
- `ul > li { list-style-type: disc; padding-left: 2em; }`
|
||||
- `ol > li { list-style-type: decimal; padding-left: 2em; }`
|
||||
- Create a specific unit test in `src/tests/unit_tests/uihtml_tests.cpp` (or a dedicated layout test) to verify `list-style-type: decimal` correctly increments numbers and `list-style-type: disc` draws a bullet.
|
||||
- **Validation:** Compile and run all tests. Must pass. (Snapshot)
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This document outlines the architectural plan for decoupling layout logic from specific widgets (like `UIRichText` and `UIHTMLTable`), introducing a robust generic layouter system, and supporting standard CSS `display` and `position` properties.
|
||||
|
||||
**AGENT DIRECTIVE (CRITICAL):** You are Negen. Fulfill this plan iteratively. You MUST compile and run the unit tests (`bin/unit_tests/eepp-unit_tests-debug`) after EVERY step. Do NOT proceed to the next step if there is even a 1-pixel difference in visual layout tests. Take a git stash snapshot (`git stash push -m "Phase X.Y passed"`) upon passing a step.
|
||||
**AGENT DIRECTIVE (CRITICAL):** You are Negen. Fulfill this plan iteratively. You MUST compile and run the unit tests (`bin/unit_tests/eepp-unit_tests-debug`) after EVERY step. Do NOT proceed to the next step if there is even a 1-pixel difference in visual layout tests. Take a git stash snapshot (`git stash push -m "Phase X.Y passed" && git stash apply`) upon passing a step to keep a checkpoint while continuing to work. **If you need to restore a stash, use `git stash apply` instead of `git stash pop` so the stable snapshot is never lost.**
|
||||
|
||||
---
|
||||
|
||||
|
||||
33
.agent/rules/html-layout-architecture.md
Normal file
33
.agent/rules/html-layout-architecture.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# HTML Layout Architecture
|
||||
|
||||
This document describes the decoupled HTML/CSS layout engine architecture implemented in `eepp` for `UIHTMLWidget` and related classes.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### 1. UIHTMLWidget
|
||||
`UIHTMLWidget` is the base class for all HTML-like elements. It holds parsed CSS properties (Display, Position, Float, Clear, etc.). Instead of implementing complex layout math directly, it queries a `UILayouterManager` to instantiate the appropriate `UILayouter` based on its `CSSDisplay` property.
|
||||
|
||||
### 2. Layouters
|
||||
Layout math has been extracted from widgets into stateless (or locally stateful) "Layouters":
|
||||
- **BlockLayouter:** Handles `CSSDisplay::Block`. It positions block-level children vertically. For rich text, it delegates text shaping to the `RichText` engine and simply maps physical coordinates for custom inline widgets.
|
||||
- **TableLayouter:** Handles `CSSDisplay::Table`. Encapsulates HTML table column width distribution and row positioning.
|
||||
- **InlineLayouter:** Handles `CSSDisplay::Inline`. *This layouter is empty by design.* Inline formatting (like `<span>` or `<a>`) is completely managed by the nearest Block container (via the `RichText` engine). It acts as a no-op so standard linear layout logic doesn't override text flows.
|
||||
- **NoneLayouter:** Handles `CSSDisplay::None`. Skips all layout and rendering.
|
||||
|
||||
### 3. The UIRichText Engine Integration
|
||||
`UIRichText` acts as the primary block container for mixed text and widget content.
|
||||
- It uses `rebuildRichText()` to recursively traverse its children.
|
||||
- Pure text nodes (`UITextSpan`, `<br>`) are appended to the core `RichText` engine via `RichText::addSpan()`.
|
||||
- Arbitrary inline widgets (e.g., `<input>`, `<button>`, or images) are passed to the engine via `RichText::addCustomSize()`.
|
||||
- After `RichText` performs line-wrapping, `BlockLayouter` iterates over the resulting `CustomBlock`s and calls `setPixelsPosition()` on those child widgets to match where the engine placed them.
|
||||
|
||||
### 4. Pixel (dp) Math strictly enforced
|
||||
All layouters **MUST** use Pixel (`Px`) variants of size and padding APIs.
|
||||
- Use `getPixelsSize()`, `getPixelsPadding()`, and `getLayoutPixelsMargin()`.
|
||||
- Never use `getSize()` or `getPadding()`, as these return density-independent pixels (dp) and will cause severe calculation bugs on HiDPI displays if mixed with pixel calculations.
|
||||
|
||||
### 5. CSS Position (Out-Of-Flow)
|
||||
Elements with `position: absolute` or `position: fixed`:
|
||||
- Are ignored by standard Layouters and `UIRichText::rebuildRichText()`.
|
||||
- Are positioned at the end of the parent's `updateLayout()` using `positionOutOfFlowChildren()`.
|
||||
- Absolute elements are positioned relative to their `getContainingBlock()` (the nearest positioned ancestor). Fixed elements map to the `UISceneNode` root.
|
||||
@@ -46,10 +46,18 @@ class EE_API UIHTMLWidget : public UILayout {
|
||||
const Uint32& state = 0 ) const;
|
||||
virtual bool applyProperty( const StyleSheetProperty& attribute );
|
||||
|
||||
virtual void updateLayout();
|
||||
|
||||
UIWidget* getContainingBlock();
|
||||
|
||||
void positionOutOfFlowChildren();
|
||||
|
||||
virtual RichText* getRichTextPtr() { return nullptr; }
|
||||
|
||||
virtual void invalidateIntrinsicSize();
|
||||
|
||||
bool isOutOfFlow() const;
|
||||
|
||||
protected:
|
||||
CSSDisplay mDisplay{ CSSDisplay::Block };
|
||||
CSSPosition mPosition{ CSSPosition::Static };
|
||||
|
||||
@@ -238,7 +238,10 @@ void BlockLayouter::positionRichTextChildren( Graphics::RichText* rt ) {
|
||||
child = mContainer->getFirstChild();
|
||||
while ( NULL != child ) {
|
||||
if ( child->isWidget() ) {
|
||||
processWidget( child->asType<UIWidget>(), processWidget );
|
||||
bool isOutOfFlow = child->isType( UI_TYPE_HTML_WIDGET ) &&
|
||||
child->asType<UIHTMLWidget>()->isOutOfFlow();
|
||||
if ( !isOutOfFlow )
|
||||
processWidget( child->asType<UIWidget>(), processWidget );
|
||||
}
|
||||
child = child->getNextNode();
|
||||
}
|
||||
|
||||
@@ -76,6 +76,8 @@ void TableLayouter::computeIntrinsicWidths() {
|
||||
auto getRecursiveSpecifiedWidth = [&]( auto&& self, Node* node ) -> Float {
|
||||
if ( !node->isWidget() )
|
||||
return 0.f;
|
||||
if ( node->isType( UI_TYPE_HTML_WIDGET ) && node->asType<UIHTMLWidget>()->isOutOfFlow() )
|
||||
return 0.f;
|
||||
UIWidget* widget = node->asType<UIWidget>();
|
||||
Float spec = 0.f;
|
||||
if ( widget->getLayoutWidthPolicy() == SizePolicy::Fixed )
|
||||
|
||||
@@ -149,10 +149,78 @@ bool UIHTMLWidget::applyProperty( const StyleSheetProperty& attribute ) {
|
||||
return UILayout::applyProperty( attribute );
|
||||
}
|
||||
|
||||
void UIHTMLWidget::updateLayout() {
|
||||
if ( getLayouter() )
|
||||
getLayouter()->updateLayout();
|
||||
else
|
||||
UILayout::updateLayout();
|
||||
|
||||
positionOutOfFlowChildren();
|
||||
}
|
||||
|
||||
UIWidget* UIHTMLWidget::getContainingBlock() {
|
||||
if ( mPosition == CSSPosition::Fixed ) {
|
||||
Node* parent = getParent();
|
||||
UIWidget* lastWidget = parent && parent->isWidget() ? parent->asType<UIWidget>() : nullptr;
|
||||
while ( parent ) {
|
||||
if ( parent->isWidget() )
|
||||
lastWidget = parent->asType<UIWidget>();
|
||||
parent = parent->getParent();
|
||||
}
|
||||
return lastWidget;
|
||||
}
|
||||
|
||||
Node* parent = getParent();
|
||||
UIWidget* lastWidget = nullptr;
|
||||
while ( parent ) {
|
||||
if ( parent->isWidget() ) {
|
||||
lastWidget = parent->asType<UIWidget>();
|
||||
if ( lastWidget->isType( UI_TYPE_HTML_WIDGET ) ) {
|
||||
if ( lastWidget->asType<UIHTMLWidget>()->getCSSPosition() != CSSPosition::Static ) {
|
||||
return lastWidget;
|
||||
}
|
||||
}
|
||||
}
|
||||
parent = parent->getParent();
|
||||
}
|
||||
return lastWidget;
|
||||
}
|
||||
|
||||
void UIHTMLWidget::positionOutOfFlowChildren() {
|
||||
Node* child = mChild;
|
||||
while ( child ) {
|
||||
if ( child->isWidget() && child->isType( UI_TYPE_HTML_WIDGET ) ) {
|
||||
UIHTMLWidget* htmlChild = static_cast<UIHTMLWidget*>( child );
|
||||
CSSPosition pos = htmlChild->getCSSPosition();
|
||||
if ( pos == CSSPosition::Absolute || pos == CSSPosition::Fixed ) {
|
||||
UIWidget* cb = htmlChild->getContainingBlock();
|
||||
if ( cb ) {
|
||||
Rectf offsets = htmlChild->getOffsets();
|
||||
Float top = PixelDensity::dpToPx( offsets.Top );
|
||||
Float left = PixelDensity::dpToPx( offsets.Left );
|
||||
|
||||
Vector2f cbPos( cb->getPixelsPadding().Left, cb->getPixelsPadding().Top );
|
||||
cbPos.x += left;
|
||||
cbPos.y += top;
|
||||
|
||||
Vector2f worldPos = cb->convertToWorldSpace( cbPos );
|
||||
Vector2f localPos = convertToNodeSpace( worldPos );
|
||||
htmlChild->setPixelsPosition( localPos );
|
||||
}
|
||||
}
|
||||
}
|
||||
child = child->getNextNode();
|
||||
}
|
||||
}
|
||||
|
||||
void UIHTMLWidget::invalidateIntrinsicSize() {
|
||||
if ( mLayouter )
|
||||
mLayouter->invalidateIntrinsicWidths();
|
||||
UIWidget::invalidateIntrinsicSize();
|
||||
}
|
||||
|
||||
bool UIHTMLWidget::isOutOfFlow() const {
|
||||
return mPosition == CSSPosition::Absolute || mPosition == CSSPosition::Fixed;
|
||||
}
|
||||
|
||||
}} // namespace EE::UI
|
||||
|
||||
@@ -593,9 +593,10 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
|
||||
}
|
||||
Node* spanChild = span->getFirstChild();
|
||||
while ( spanChild != NULL ) {
|
||||
if ( spanChild->isWidget() ) {
|
||||
bool isOutOfFlow = spanChild->isType( UI_TYPE_HTML_WIDGET ) &&
|
||||
spanChild->asType<UIHTMLWidget>()->isOutOfFlow();
|
||||
if ( !isOutOfFlow )
|
||||
processWidgetRef( spanChild->asType<UIWidget>(), processWidgetRef );
|
||||
}
|
||||
spanChild = spanChild->getNextNode();
|
||||
}
|
||||
} else if ( widget->isType( UI_TYPE_BR ) ) {
|
||||
@@ -649,9 +650,10 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
|
||||
|
||||
Node* child = container->getFirstChild();
|
||||
while ( NULL != child ) {
|
||||
if ( child->isWidget() ) {
|
||||
bool isOutOfFlow =
|
||||
child->isType( UI_TYPE_HTML_WIDGET ) && child->asType<UIHTMLWidget>()->isOutOfFlow();
|
||||
if ( !isOutOfFlow )
|
||||
processWidget( child->asType<UIWidget>(), processWidget );
|
||||
}
|
||||
child = child->getNextNode();
|
||||
}
|
||||
}
|
||||
|
||||
198
src/tests/unit_tests/uihtml_position_tests.cpp
Normal file
198
src/tests/unit_tests/uihtml_position_tests.cpp
Normal file
@@ -0,0 +1,198 @@
|
||||
#include "utest.h"
|
||||
#include <eepp/ee.hpp>
|
||||
#include <eepp/scene/scenemanager.hpp>
|
||||
|
||||
using namespace EE;
|
||||
using namespace EE::UI;
|
||||
using namespace EE::Window;
|
||||
using namespace EE::Graphics;
|
||||
|
||||
static void init_ui_test() {
|
||||
Engine::instance()->createWindow(
|
||||
WindowSettings( 1024, 650, "UIHTMLWidget Position Out Of Flow 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" );
|
||||
FontFamily::loadFromRegular( font );
|
||||
|
||||
UI::UISceneNode* sceneNode = UI::UISceneNode::New();
|
||||
SceneManager::instance()->add( sceneNode );
|
||||
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
||||
themeManager->setDefaultFont( font );
|
||||
}
|
||||
|
||||
UTEST( UIHTMLWidget, positionOutOfFlow_AbsoluteRelToRoot ) {
|
||||
init_ui_test();
|
||||
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
||||
|
||||
UIHTMLWidget* rootContainer = UIHTMLWidget::New();
|
||||
rootContainer->setParent( sceneNode->getRoot() );
|
||||
rootContainer->setPixelsSize( 500, 500 );
|
||||
rootContainer->setPixelsPosition( 100, 100 );
|
||||
|
||||
UIHTMLWidget* staticChild = UIHTMLWidget::New();
|
||||
staticChild->setParent( rootContainer );
|
||||
staticChild->setPixelsSize( 100, 100 );
|
||||
staticChild->setPixelsPosition( 50, 50 );
|
||||
|
||||
UIHTMLWidget* absoluteChild = UIHTMLWidget::New();
|
||||
absoluteChild->setParent( staticChild );
|
||||
absoluteChild->setCSSPosition( CSSPosition::Absolute );
|
||||
absoluteChild->setOffsets( Rectf( 25, 15, 0, 0 ) ); // L, T, R, B
|
||||
absoluteChild->setPixelsSize( 50, 50 );
|
||||
|
||||
sceneNode->updateDirtyLayouts();
|
||||
|
||||
UIWidget* cb = absoluteChild->getContainingBlock();
|
||||
EXPECT_EQ( cb, sceneNode->getRoot() ); // Because no relative ancestor
|
||||
|
||||
Vector2f worldPos = absoluteChild->convertToWorldSpace( { 0, 0 } );
|
||||
EXPECT_NEAR( 25.f, worldPos.x, 1.f );
|
||||
EXPECT_NEAR( 15.f, worldPos.y, 1.f );
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIHTMLWidget, positionOutOfFlow_AbsoluteRelToRelative ) {
|
||||
init_ui_test();
|
||||
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
||||
|
||||
UIHTMLWidget* rootContainer = UIHTMLWidget::New();
|
||||
rootContainer->setParent( sceneNode->getRoot() );
|
||||
rootContainer->setCSSPosition( CSSPosition::Relative );
|
||||
rootContainer->setPixelsSize( 500, 500 );
|
||||
rootContainer->setPixelsPosition( 100, 100 );
|
||||
rootContainer->setPadding( Rectf( 10, 20, 30, 40 ) ); // L, T, R, B
|
||||
|
||||
UIHTMLWidget* staticChild = UIHTMLWidget::New();
|
||||
staticChild->setParent( rootContainer );
|
||||
staticChild->setPixelsSize( 100, 100 );
|
||||
staticChild->setPixelsPosition( 50, 50 );
|
||||
|
||||
UIHTMLWidget* absoluteChild = UIHTMLWidget::New();
|
||||
absoluteChild->setParent( staticChild );
|
||||
absoluteChild->setCSSPosition( CSSPosition::Absolute );
|
||||
absoluteChild->setOffsets( Rectf( 25, 15, 0, 0 ) ); // L, T, R, B
|
||||
absoluteChild->setPixelsSize( 50, 50 );
|
||||
|
||||
sceneNode->updateDirtyLayouts();
|
||||
|
||||
UIWidget* cb = absoluteChild->getContainingBlock();
|
||||
EXPECT_EQ( cb, rootContainer );
|
||||
|
||||
Vector2f worldPos = absoluteChild->convertToWorldSpace( { 0, 0 } );
|
||||
// rootContainer world pos is 100, 100
|
||||
// cb padding left = 10, top = 20
|
||||
// absoluteChild offset left = 25, top = 15
|
||||
// worldPos should be 100 + 10 + 25 = 135
|
||||
// worldPos y should be 100 + 20 + 15 = 135
|
||||
EXPECT_NEAR( 135.f, worldPos.x, 1.f );
|
||||
EXPECT_NEAR( 135.f, worldPos.y, 1.f );
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIHTMLWidget, positionOutOfFlow_NestedAbsolute ) {
|
||||
init_ui_test();
|
||||
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
||||
|
||||
UIHTMLWidget* relContainer = UIHTMLWidget::New();
|
||||
relContainer->setParent( sceneNode->getRoot() );
|
||||
relContainer->setCSSPosition( CSSPosition::Relative );
|
||||
relContainer->setPixelsSize( 500, 500 );
|
||||
relContainer->setPixelsPosition( 100, 100 );
|
||||
|
||||
UIHTMLWidget* absContainer = UIHTMLWidget::New();
|
||||
absContainer->setParent( relContainer );
|
||||
absContainer->setCSSPosition( CSSPosition::Absolute );
|
||||
absContainer->setOffsets( Rectf( 50, 50, 0, 0 ) );
|
||||
absContainer->setPixelsSize( 200, 200 );
|
||||
|
||||
UIHTMLWidget* absChild = UIHTMLWidget::New();
|
||||
absChild->setParent( absContainer );
|
||||
absChild->setCSSPosition( CSSPosition::Absolute );
|
||||
absChild->setOffsets( Rectf( 20, 20, 0, 0 ) );
|
||||
absChild->setPixelsSize( 50, 50 );
|
||||
|
||||
sceneNode->updateDirtyLayouts();
|
||||
|
||||
UIWidget* cb1 = absContainer->getContainingBlock();
|
||||
EXPECT_EQ( cb1, relContainer );
|
||||
|
||||
UIWidget* cb2 = absChild->getContainingBlock();
|
||||
EXPECT_EQ( cb2, absContainer );
|
||||
|
||||
Vector2f worldPos1 = absContainer->convertToWorldSpace( { 0, 0 } );
|
||||
EXPECT_NEAR( 150.f, worldPos1.x, 1.f );
|
||||
EXPECT_NEAR( 150.f, worldPos1.y, 1.f );
|
||||
|
||||
Vector2f worldPos2 = absChild->convertToWorldSpace( { 0, 0 } );
|
||||
EXPECT_NEAR( 170.f, worldPos2.x, 1.f );
|
||||
EXPECT_NEAR( 170.f, worldPos2.y, 1.f );
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIHTMLWidget, positionOutOfFlow_Fixed ) {
|
||||
init_ui_test();
|
||||
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
||||
|
||||
UIHTMLWidget* relContainer = UIHTMLWidget::New();
|
||||
relContainer->setParent( sceneNode->getRoot() );
|
||||
relContainer->setCSSPosition( CSSPosition::Relative );
|
||||
relContainer->setPixelsSize( 500, 500 );
|
||||
relContainer->setPixelsPosition( 100, 100 );
|
||||
|
||||
UIHTMLWidget* staticChild = UIHTMLWidget::New();
|
||||
staticChild->setParent( relContainer );
|
||||
staticChild->setPixelsSize( 100, 100 );
|
||||
staticChild->setPixelsPosition( 50, 50 );
|
||||
|
||||
UIHTMLWidget* fixedChild = UIHTMLWidget::New();
|
||||
fixedChild->setParent( staticChild );
|
||||
fixedChild->setCSSPosition( CSSPosition::Fixed );
|
||||
fixedChild->setOffsets( Rectf( 30, 40, 0, 0 ) ); // L, T, R, B
|
||||
fixedChild->setPixelsSize( 50, 50 );
|
||||
|
||||
sceneNode->updateDirtyLayouts();
|
||||
|
||||
UIWidget* cbFixed = fixedChild->getContainingBlock();
|
||||
EXPECT_EQ( cbFixed, sceneNode->getRoot() );
|
||||
|
||||
Vector2f worldPos = fixedChild->convertToWorldSpace( { 0, 0 } );
|
||||
EXPECT_NEAR( 30.f, worldPos.x, 1.f );
|
||||
EXPECT_NEAR( 40.f, worldPos.y, 1.f );
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIHTMLWidget, positionOutOfFlow_DoesNotAffectParentSize ) {
|
||||
init_ui_test();
|
||||
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
||||
|
||||
UIRichText* autoSizedParent = UIRichText::New();
|
||||
autoSizedParent->setParent( sceneNode->getRoot() );
|
||||
autoSizedParent->setCSSPosition( CSSPosition::Relative );
|
||||
autoSizedParent->setLayoutSizePolicy( SizePolicy::WrapContent, SizePolicy::WrapContent );
|
||||
|
||||
UIWidget* normalChild = UIWidget::New();
|
||||
normalChild->setParent( autoSizedParent );
|
||||
normalChild->setPixelsSize( 100, 100 );
|
||||
normalChild->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
|
||||
|
||||
UIHTMLWidget* absoluteChild = UIHTMLWidget::New();
|
||||
absoluteChild->setParent( autoSizedParent );
|
||||
absoluteChild->setCSSPosition( CSSPosition::Absolute );
|
||||
absoluteChild->setPixelsSize( 5000, 5000 ); // Very large
|
||||
absoluteChild->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
|
||||
|
||||
sceneNode->updateDirtyLayouts();
|
||||
|
||||
// The parent's size should only encompass the normal child
|
||||
EXPECT_NEAR( 100.f, autoSizedParent->getPixelsSize().getWidth(), 1.f );
|
||||
EXPECT_NEAR( 100.f, autoSizedParent->getPixelsSize().getHeight(), 1.f );
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
Reference in New Issue
Block a user