Greatly improved UIWebView implementation.

This commit is contained in:
Martín Lucas Golini
2026-07-01 20:36:00 -03:00
parent 38448008ec
commit 4c73311789
7 changed files with 1162 additions and 233 deletions
@@ -0,0 +1,491 @@
# UIWebView Document Scene Layout Refactor Plan
> Status: REVISED - use a real layout widget as the scroll target, while keeping the
> owned `UISceneNode` nested under that widget as the document boundary.
>
> This supersedes the earlier "make `UISceneNode` behave as a layout" attempt and
> corrects the overcorrection where the scene was removed from the widget tree entirely.
---
## Lessons From Failed Attempts
The first approach tried to make `UISceneNode` participate directly in the layout system:
1. Override `isLayout()` on `UISceneNode`.
2. Give it layout policies such as `MatchParent` width and `WrapContent` height.
3. Let the scene root (`mRoot`, a `UIRoot`) act as the document container.
That approach hit real class-hierarchy dead ends.
### Dead End 1: UISceneNode Is Not A UIWidget
`UISceneNode` inherits from `SceneNode`. `UILayout` inherits from `UIWidget`. Returning
`true` from `isLayout()` does not make layout APIs available: `updateLayoutTree()`,
`setInternalPixelsSize()`, layout margins, padding, and size policies are widget/layout
contracts that `SceneNode` does not implement.
### Dead End 2: UIRoot Is Not A UILayout
Making `mRoot` the document container also fails. `UIRoot` extends `UIWidget`, not
`UILayout`; it does not auto-pack children. Once content grows, manual size propagation
is still required.
### Dead End 3: Scroll Extent Became Layout Viewport
The document has at least three separate metrics:
- **CSS viewport:** the visible `UIScrollView::mContainer` size. Media queries,
`vw`/`vh`, fixed positioning, sticky positioning, and document minimum height use it.
- **Layout viewport / initial containing block:** the viewport-sized reference used by
root/body auto-width layout and percentage descendants.
- **Scrollable overflow extent:** the measured document overflow size used by
`UIScrollView` for scroll ranges.
When the scene itself is the scroll target, `UISceneNode::onSizeChange()` naturally
resizes `mRoot` to the scene extent. That makes scroll extent become the containing
block, so a previous wide document can keep responsive content artificially wide after
the web view shrinks.
### Dead End 4: Removing Viewport Style Invalidation Breaks CSS Units
Viewport-dependent style values such as `height: 100vh` and media query results must
recompute when the viewport changes. A full `reloadStyle(true, true, true)` is expensive,
but removing viewport invalidation entirely is incorrect. The implementation needs a
targeted viewport/style/layout dirty path.
### Dead End 5: Removing Event-Driven Viewport Sync Causes Multi-Step Resize
Container size and scrollbar visibility changes must be observed promptly. Relying only
on a later scheduled update can produce visible intermediate states at the old viewport
width. The fix is not per-frame polling; it is a dirty, synchronous style/layout flush
for viewport geometry changes that does not run actions or arbitrary node updates.
### Dead End 6: Scene Outside The Widget Tree Breaks Ownership
The inverted plan originally proposed a scene owned internally by a layout widget but
not present in the widget tree. That avoids `UISceneNode` as scroll target, but creates
new hard problems:
- document widgets no longer naturally return the document scene from `getUISceneNode()`
and `getSceneNode()`;
- rendering must be manually forwarded or redirected;
- input hit testing must be manually bridged;
- focus and invalidation need custom routing;
- inspector/debug traversal becomes special-case heavy.
That is too much new machinery for the first correct `UIWebView` implementation.
---
## Corrected Architecture
Use a real layout widget as the scroll target, but keep the document scene nested in
the widget tree below that scroll target.
```text
application UISceneNode
└── ... application widgets ...
└── UIWebView
└── UIScrollView::mContainer host-scene clipped viewport
└── UIWebView::mDocumentLayout UILayout scroll target, scroll-extent sized
└── UIWebView::mDocumentScene owned UISceneNode, document boundary
└── UISceneNode::mRoot root sized by layout viewport policy
└── UIWebView::mDocContainer document layout container
└── html
└── body
└── document content
```
### Key Decisions
1. **`mDocumentLayout` is the scroll target.** It is a normal layout/widget child of
`UIWebView`, so `UIScrollView::onChildCountChange()` reparents it into
`mContainer` and uses it as `mScrollView`.
2. **`mDocumentScene` remains in the widget tree.** It is a child of
`mDocumentLayout`, not a direct child of `UIWebView`. This preserves rendering,
hit testing, invalidation propagation, focus routing through the shared dispatcher,
and the core invariant that document widgets resolve to the document scene.
3. **`mDocumentLayout` owns scroll extent.** Its pixel size is the size observed by
`UIScrollView` for scrollbar calculations. It must be at least the visible viewport
and may grow from measured overflow.
4. **`mDocumentScene` owns document state.** Stylesheet, URI, referer, cookies,
navigation callback, dirty queues, actions, keyframes, and author font aliases remain
scene-local.
5. **The scene root must not inherit scroll extent as its layout viewport.** The scene
may need a scroll-target size for world bounds and hit testing, but `mRoot`/HTML/body
layout must use the layout viewport, not the measured overflow extent. Implement this
with explicit viewport/layout-viewport metrics or a root-sizing policy on embedded
document scenes.
6. **Manual extent measurement remains, but it is dirty-driven.** Horizontal overflow,
images, fonts, tables, fixed-width content, and replaced controls still need measured
scroll extent. The difference is that extent measurement is tied to explicit dirty
signals, not a full descendant-tree scan every frame.
---
## Why This Architecture Is Better
| Problem | Failed scene-as-layout approach | Corrected approach |
|---|---|---|
| `UISceneNode` is not a layout | Tried to make it act like `UILayout` | `mDocumentLayout` is the layout/scroll target |
| `UIRoot` is not a layout | Tried to make root auto-pack content | `mDocContainer` remains a real `UILinearLayout` |
| Scroll extent vs viewport | Scene size resized root to extent | Layout viewport and scroll extent are separate |
| Document isolation | Owned scene provided isolation | Owned nested scene still provides isolation |
| Rendering/input | Worked because scene was in tree | Still works because scene remains in tree |
| Resize responsiveness | Needed event sync | Uses event dirtying plus narrow style/layout flush |
| Performance | Tended toward per-frame scans/cascades | Dirty-driven extent measurement |
---
## Current Implementation Status
### Implemented
- `UIWebView` now creates `mDocumentLayout` as the real `UIScrollView` scroll target.
- The owned `mDocumentScene` is nested under `mDocumentLayout`, so document widgets
still resolve to the document scene while normal scroll-view clipping and scrolling
operate on a widget/layout node.
- `UISceneNode` has explicit CSS viewport and layout viewport metrics. Embedded
document scenes can keep `mRoot` viewport-sized while the scene/layout scroll extent
grows to the measured document overflow.
- `UISceneNode::flushDirtyStyleAndLayout()` exists as the narrow dirty flush. It
processes dirty styles, style states, and layouts without running actions, timers,
scheduled updates, or a full scene update.
- `UIWebView` has dirty-driven document metric updates through
`markDocumentExtentDirty()` / `updateDocumentMetricsIfNeeded()`.
- Viewport changes are coalesced into the scheduled update path. The CSS viewport is
derived from current web-view geometry and scrollbar visibility, not from queued
`UIScrollView::mContainer` size updates.
- Scroll extent measurement resizes `mDocumentLayout` and `mDocumentScene`, while
root/html/body layout remains viewport-sized.
- Tests cover the new topology, viewport-vs-extent behavior, scrolling, two-scene
style isolation, navigation supersession, and a resize metric regression that guards
against no-op queued viewport churn rebuilding RichText.
### Pending / Follow-Up
- **Cleaner hit-test bounds model.** The current implementation overrides
`UISceneNode::overFind()` for scenes with an explicit layout viewport. This is a
narrow compatibility hook: when the viewport-sized `mRoot` rejects a point, the scene
searches root children directly so visible scrolled document descendants can still be
hit. The cleaner long-term shape is to split layout bounds from hit-test traversal
bounds, likely on `UIRoot` or `Node`, so root can remain the layout viewport while
hit testing can intentionally traverse visible overflow without a `UISceneNode`
special case.
- **Author `@font-face` isolation and cleanup** still needs the full scene-local alias
registry and navigation cleanup described in Phase 5.
- **Subresource lifetime coverage** should be completed for every async path described
in Phase 6, including deferred CSS, fonts, images, redirects, cookies, and destruction.
- **Example and documentation integration** should be completed after the code shape
settles, especially `.agent/rules/html-layout-architecture.md` and the HTML example
stylesheet injection path.
- **Fixed/sticky positioning coverage** is still listed as required test coverage for
the final architecture. Existing viewport tests cover the core sizing behavior, but
fixed/sticky document behavior should remain an explicit acceptance item.
---
## Required Supporting API
### UISceneNode Viewport And Root Sizing
Keep or add explicit viewport metrics:
```cpp
void setViewportPixelsSize( const Sizef& size );
void clearViewportPixelsSize();
const Sizef& getViewportPixelsSize() const;
```
Add one of the following:
```cpp
void setLayoutViewportPixelsSize( const Sizef& size );
void clearLayoutViewportPixelsSize();
const Sizef& getLayoutViewportPixelsSize() const;
```
or an equivalent embedded-document root sizing policy. The invariant is more important
than the exact API: setting the scrollable extent must not resize `mRoot` into the
normal-flow containing block.
### Narrow Dirty Flush
Add a narrow flush API:
```cpp
void UISceneNode::flushDirtyStyleAndLayout();
```
It should process dirty styles, style states, and layouts using the same invalidation
depth semantics as `UISceneNode::update()`, but it must not call `SceneNode::update()`
and must not run actions, timers, scheduled updates, or arbitrary node update callbacks.
`UIWebView` uses this after viewport geometry changes and before measuring scroll
extent.
### Document Extent Dirtying
Add dirty state owned by `UIWebView`:
```cpp
bool mDocumentExtentDirty{ true };
LayoutInvalidationFlags mDocumentExtentDirtyReasons{};
void markDocumentExtentDirty( LayoutInvalidationFlags reasons );
void updateDocumentMetricsIfNeeded();
```
Mark extent dirty when:
- `mContainer` size changes;
- scrollbar visibility changes and the scroll view type consumes viewport space;
- document children are loaded or closed;
- inline or external CSS applies;
- media queries change;
- images/replaced controls change intrinsic size;
- author fonts load;
- layout invalidation includes document, viewport, or overflow-affecting reasons;
- table/flex/grid/rich-text layout reports size-affecting changes.
Do not recompute document extent from the normal scheduled update path unless this flag
is set.
---
## Implementation Plan
### Phase 1: Add DocumentLayout Scroll Target
**Files:**
- `include/eepp/ui/uiwebview.hpp`
- `src/eepp/ui/uiwebview.cpp`
Steps:
1. Add `UILayout* mDocumentLayout` or a narrow internal subclass if a hook is needed.
2. Construct `mDocumentLayout` as the only direct scroll child of `UIWebView`.
3. Give `mDocumentLayout` `MatchParent` width and `WrapContent` height defaults.
4. Move `mDocumentScene` under `mDocumentLayout`, not directly under `UIWebView`.
5. Keep `mDocContainer` under `mDocumentScene->getRoot()`.
6. Ensure `mDocContainer`, `html`, `body`, and descendants still return
`mDocumentScene` from `getUISceneNode()` and `getSceneNode()`.
Tests:
- `UIScrollView::getScrollView()` returns `mDocumentLayout`.
- `mDocumentScene->getParent()` is `mDocumentLayout`.
- Document descendants belong to the document scene.
- The document scene is not registered with `SceneManager`.
### Phase 2: Separate Viewport, Layout Viewport, And Scroll Extent
**Files:**
- `include/eepp/ui/uiscenenode.hpp`
- `src/eepp/ui/uiscenenode.cpp`
- `src/eepp/ui/uinode.cpp`
- `src/eepp/ui/uiwebview.cpp`
Steps:
1. Keep `getViewportPixelsSize()` for media queries and `vw`/`vh`.
2. Add layout-viewport/root-sizing support so `mRoot` and normal root/body layout use
the visible viewport even when scroll extent is larger.
3. Update `UINode::convertLength()` so viewport units resolve through the document
scene viewport.
4. Update HTML/body minimum-height handling to use the CSS viewport.
5. Ensure fixed/sticky positioning uses the visible scroll viewport, not measured
document extent.
Tests:
- `100vh` resolves to the visible viewport height while scroll extent is much taller.
- After growing wide and shrinking, `html`, `body`, `width:100vw`, and `width:100%`
content resolve to the new viewport.
- Explicit fixed-width overflow still creates horizontal scroll.
- `position: fixed` remains pinned to the web-view viewport while scrolling.
- `position: sticky` uses the web-view viewport while scrolling.
### Phase 3: Add Narrow Dirty Flush And Dirty-Driven Extent Measurement
**Files:**
- `include/eepp/ui/uiscenenode.hpp`
- `src/eepp/ui/uiscenenode.cpp`
- `include/eepp/ui/uiwebview.hpp`
- `src/eepp/ui/uiwebview.cpp`
Steps:
1. Add `UISceneNode::flushDirtyStyleAndLayout()`.
2. Add `UIWebView::markDocumentExtentDirty()`.
3. Add `UIWebView::updateDocumentMetricsIfNeeded()`.
4. On viewport geometry changes, update scene viewport/layout viewport, dirty
viewport-dependent document layout, flush style/layout, measure overflow, resize
`mDocumentLayout`, then update scrollbars.
5. In `scheduledUpdate()`, call `UITouchDraggableWidget::scheduledUpdate(time)`, then
update the document scene exactly once. After that, call
`updateDocumentMetricsIfNeeded()` only if dirty.
6. Remove any use of `mDocumentScene->update(Time::Zero)` for synchronous layout
settlement.
7. Avoid full descendant-tree extent scans when no dirty signal occurred.
Tests:
- Viewport change settles in one frame without a second full scene update.
- Actions/timers/scheduled node updates are not run by `flushDirtyStyleAndLayout()`.
- A stable large document does not remeasure extent every frame.
- Hidden and clipped wide descendants do not create horizontal scroll.
### Phase 4: Route Document Operations Through The Owned Scene
**Files:**
- `src/eepp/ui/uiwebview.cpp`
- `src/examples/ui_html/ui_html.cpp`
Steps:
1. Use `mDocumentScene` for stylesheet cleanup, URI/referer, cookie manager,
navigation interception, document loading, relative URL resolution, and HTML/body
lookup.
2. On navigation, close only `mDocContainer` children.
3. Remove only document-scene nonpersistent stylesheet rules.
4. Clear previous document-local author font aliases/resources.
5. Mark document extent dirty after navigation and after document CSS injection.
6. Install the document navigation interceptor once during scene setup, not on every
document load.
7. Update examples to combine document CSS into
`webView->getDocumentSceneNode()`.
Tests:
- Two web views with conflicting selectors, IDs, CSS variables, keyframes, and
`@font-face` names remain isolated.
- Navigating one web view does not mutate the other web view or the host scene.
- Application-level HTML lookup does not find web-view document nodes.
- Document-scene lookup does find document nodes.
- External CSS with relative URLs resolves from the correct document URI.
### Phase 5: Author Font-Face Isolation And Cleanup
**Files:**
- `include/eepp/ui/uiscenenode.hpp`
- `src/eepp/ui/uiscenenode.cpp`
Steps:
1. Add a scene-local font-face alias registry keyed by CSS family, style, and weight.
2. Register loaded author fonts under scene-unique internal names.
3. Resolve author font aliases before global `FontManager` fallback.
4. Add `clearAuthorFontFaces()` and call it during navigation.
5. Remove only this scene's internally registered author fonts during scene destruction.
6. Mark document extent dirty after a font load can affect metrics.
Tests:
- Two documents can declare the same family name with different files.
- Navigating one document clears and replaces only its own author font alias.
- Destroying one web view does not remove sibling fonts.
- Global application/system font fallback still works.
### Phase 6: Lifetime-Safe Navigation And Subresources
**Files:**
- `include/eepp/ui/uiwebview.hpp`
- `src/eepp/ui/uiwebview.cpp`
- `include/eepp/ui/uiscenenode.hpp`
- `src/eepp/ui/uiscenenode.cpp`
Steps:
1. Add navigation generation state for top-level document loads.
2. Add scene-local async resource generation state for subresources.
3. Invalidate document responses and subresources on navigation.
4. Invalidate everything on destruction.
5. Apply generation checks before cookies, style mutation, font registration, image
mutation, main-thread repost mutation, navigation events, or extent dirtying.
6. Cover HTTP, redirects, external CSS, deferred local CSS, VFS CSS, remote/local/data
fonts, and async image/replaced resources.
Tests:
- A stale document response cannot replace a newer document.
- Stale redirects/cookies do not mutate the current document.
- Deferred local CSS after navigation/destruction is ignored safely.
- Old `@font-face` and image responses cannot mutate current document state.
### Phase 7: Cleanup, Documentation, And Integration
**Files:**
- `.agent/rules/html-layout-architecture.md`
- `.agent/plans/uiwebview_document_scene_plan.md`
- `src/tests/unit_tests/uiwebview_tests.cpp`
- `src/tests/unit_tests/uihtml_tests.cpp`
Steps:
1. Document the final tree and the three document metrics.
2. Remove obsolete viewport/extent sync methods and state booleans.
3. Keep only event listeners that mark viewport/extent dirty.
4. Update tests that assumed the document scene itself was the scroll target.
5. Verify inspector/debug tooling can target either the host scene or
`getDocumentSceneNode()`.
---
## Files Expected To Change
| File | Change |
|---|---|
| `include/eepp/ui/uiwebview.hpp` | Add document layout scroll target, dirty extent state, lifetime state |
| `src/eepp/ui/uiwebview.cpp` | Use layout widget as scroll target; route document operations to owned scene; dirty-driven extent measurement |
| `include/eepp/ui/uiscenenode.hpp` | Viewport/layout viewport metrics, narrow dirty flush, resource lifetime helpers |
| `src/eepp/ui/uiscenenode.cpp` | Root sizing policy, dirty flush, author font aliases, guarded subresource callbacks |
| `src/eepp/ui/uinode.cpp` | Resolve viewport units from document viewport |
| `src/examples/ui_html/ui_html.cpp` | Inject document CSS through document scene |
| `src/tests/unit_tests/uiwebview_tests.cpp` | Scroll target, isolation, viewport, fixed/sticky, performance, lifecycle tests |
| `src/tests/unit_tests/uihtml_tests.cpp` | Realistic smoke/regression fixture adjustments |
| `.agent/rules/html-layout-architecture.md` | Final architecture documentation |
---
## Validation
```sh
make -C make/linux -j$(nproc)
projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UIWebView.*"
projects/scripts/xvfb-run-eepp \
bin/unit_tests/eepp-unit_tests-debug --filter="UIHTML.redditOldThreadWebViewSmoke"
git diff --check
```
Run the full unit suite after focused viewport, scrolling, positioning, resource, and
two-web-view isolation tests pass:
```sh
projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug
```
---
## Completion Criteria
Two `UIWebView` instances can coexist in one application scene with independent DOM,
styles, URI/referer, navigation, cookies, font faces, resources, input, focus, layout,
and scrolling.
The scroll target is a real layout widget. The owned document scene remains nested as
the document state boundary. CSS viewport, layout viewport, and scrollable overflow
extent are distinct. Viewport changes settle through a narrow style/layout flush, not a
second scene update. Stable documents do not rescan full content every frame.
+211 -71
View File
@@ -11,8 +11,13 @@ media-query evaluation, dirty style/layout queues, and document resources must u
instead of the application `UISceneNode`. instead of the application `UISceneNode`.
The owned scene remains attached inside the application scene tree so it renders and receives input The owned scene remains attached inside the application scene tree so it renders and receives input
as part of the embedding UI. This establishes the document boundary needed for multiple independent as part of the embedding UI. A real layout widget, not the scene node itself, is the scroll target.
web views and is the foundation for future `iframe` support. This establishes the document boundary needed for multiple independent web views and is the
foundation for future `iframe` support.
This plan should be implemented together with
`.agent/plans/uiwebview_document_scene_layout_refactor.md`, which details the corrected scroll-target
architecture.
## Required Invariants ## Required Invariants
@@ -24,11 +29,17 @@ After implementation:
referer, navigation callback, dirty queues, or document lookup results. referer, navigation callback, dirty queues, or document lookup results.
- Every loaded HTML widget returns the web view's owned scene from `getUISceneNode()` and - Every loaded HTML widget returns the web view's owned scene from `getUISceneNode()` and
`getSceneNode()`. `getSceneNode()`.
- The owned scene is attached to the application tree but is not registered as a top-level - The owned scene is attached below the web view document layout widget but is not registered as a
`SceneManager` scene. top-level `SceneManager` scene.
- The owned scene size follows the scrollable HTML/body document extent. - The web view document layout widget is the `UIScrollView` scroll target and follows the scrollable
HTML/body document extent.
- The document scene separately stores the visible web-view viewport size for viewport units, media - The document scene separately stores the visible web-view viewport size for viewport units, media
queries, document minimum height, and fixed-position semantics. queries, document minimum height, and fixed-position semantics.
- The document scene distinguishes CSS viewport, layout viewport / initial containing block, and
scrollable overflow extent. Scroll extent must not become the containing block used for normal
root/body layout.
- Document scroll extent is recomputed only from explicit dirty signals, not by scanning the full
document every frame.
- Input and focus continue through the embedding application's shared `UIEventDispatcher`. - Input and focus continue through the embedding application's shared `UIEventDispatcher`.
- Closing or destroying the web view destroys its owned scene and document without leaving async - Closing or destroying the web view destroys its owned scene and document without leaving async
callbacks, actions, listeners, or dirty pointers behind. callbacks, actions, listeners, or dirty pointers behind.
@@ -70,21 +81,32 @@ An owned web-view scene must not require top-level `SceneManager` registration.
### Viewport And Scroll Extent Are Currently Conflated ### Viewport And Scroll Extent Are Currently Conflated
The owned scene must be the `UIScrollView` scroll target and must grow to the HTML/body document The owned document scene must expose the HTML/body scrollable overflow extent to a real layout-widget
extent. For example, a 600px-tall web view displaying a 3000px-tall page needs a 3000px-tall scroll target. For example, a 600px-tall web view displaying a 3000px-tall page needs a 3000px-tall
`mDocumentScene` so `UIScrollView` can derive the correct scroll range. scroll target so `UIScrollView` can derive the correct scroll range. The scroll target should be a
`UILayout`/`UIWidget` owned by `UIWebView`, not `UISceneNode` itself.
`UISceneNode` currently also uses its own size for viewport-dependent CSS behavior. Once the scene is `UISceneNode` currently also uses its own size for viewport-dependent CSS behavior. Once the document
content-sized, those uses must not read the scene extent: has a separately measured scroll extent, those uses must not read the scroll extent:
- `UISceneNode::getMediaFeatures()` must report the visible web-view viewport. - `UISceneNode::getMediaFeatures()` must report the visible web-view viewport.
- `vw`/`vh` and related viewport-relative lengths must resolve against the visible viewport. - `vw`/`vh` and related viewport-relative lengths must resolve against the visible viewport.
- HTML/body minimum height must use the visible viewport. - HTML/body minimum height must use the visible viewport.
- Fixed-position layout must remain relative to the web-view viewport while the content-sized scene - Fixed-position layout must remain relative to the web-view viewport while the scroll target is
is translated by scrolling. translated by scrolling.
The design therefore needs separate scene extent and viewport metrics. The scene extent participates The design therefore needs separate metrics:
in scrolling; the viewport size is provided by the embedding `UIWebView`.
- **CSS viewport:** the visible `UIScrollView::mContainer` size. Media queries, `vw`/`vh`, fixed
positioning, sticky positioning, and document minimum height use this size.
- **Layout viewport / initial containing block:** the viewport-sized root layout reference used for
normal root/body auto-width layout. This prevents a previously measured scroll extent from becoming
the containing block and keeping responsive content artificially wide or tall.
- **Scrollable overflow extent:** the measured document overflow size used only as the scroll target
size observed by `UIScrollView`.
The scroll extent participates in scrolling; the CSS and layout viewport are provided by the
embedding `UIWebView`.
## Proposed Scene Tree ## Proposed Scene Tree
@@ -93,20 +115,23 @@ application UISceneNode
└── ... application widgets ... └── ... application widgets ...
└── UIWebView └── UIWebView
└── UIScrollView::mContainer host-scene clipping shell └── UIScrollView::mContainer host-scene clipping shell
└── UIWebView::mDocumentScene owned UISceneNode, content-sized scroll target └── UIWebView::mDocumentLayout UILayout scroll target, scroll-extent sized
└── UISceneNode::mRoot owned scene root, content-sized └── UIWebView::mDocumentScene owned UISceneNode, document boundary
└── UIWebView::mDocContainer document layout container └── UISceneNode::mRoot owned scene root, layout-viewport sized
└── html └── UIWebView::mDocContainer
└── body └── html
└── document content └── body
└── document content
``` ```
`mDocumentScene` is added as the `UIWebView` child through the normal `UIScrollView` path. `mDocumentLayout` is added as the `UIWebView` child through the normal `UIScrollView` path.
`UIScrollView::onChildCountChange()` reparents it into the clipped `mContainer` and selects it as the `UIScrollView::onChildCountChange()` reparents it into the clipped `mContainer` and selects it as the
scroll target. `mDocumentScene` grows to the document extent and is translated when scrolling. scroll target. `mDocumentLayout` exposes the measured scrollable extent and is translated when
scrolling.
The visible `mContainer` size is separately passed to `mDocumentScene` as its CSS viewport size. The visible `mContainer` size is separately passed to `mDocumentScene` as its CSS viewport size.
Every document node, including `mDocContainer`, belongs to the owned scene. The document scene root keeps a layout-viewport size unless an explicit API says a caller is setting
the scroll target extent. Every document node, including `mDocContainer`, belongs to the owned scene.
## Ownership And Service Policy ## Ownership And Service Policy
@@ -172,7 +197,7 @@ persist across navigations inside this web view. The default remains the HTML de
## Implementation Plan ## Implementation Plan
### Phase 1: Separate UISceneNode Extent From Viewport Metrics ### Phase 1: Separate Viewport, Layout Viewport, And Scroll Extent
**Files:** **Files:**
@@ -195,24 +220,45 @@ Steps:
When no override is set, `getViewportPixelsSize()` returns the scene's own pixel size so existing When no override is set, `getViewportPixelsSize()` returns the scene's own pixel size so existing
top-level and UI editor behavior remains unchanged. top-level and UI editor behavior remains unchanged.
2. Add a narrow way to disable the nested scene's automatic parent-size following. Preserve the 2. Add explicit layout-viewport metrics or an equivalent root-sizing policy for embedded document
current default for existing nested scenes; `UIWebView` disables it because its document scene scenes:
extent is content-driven.
3. Make `UISceneNode::getMediaFeatures()` use `getViewportPixelsSize()` for viewport width/height ```cpp
void setLayoutViewportPixelsSize( const Sizef& size );
void clearLayoutViewportPixelsSize();
const Sizef& getLayoutViewportPixelsSize() const;
```
If a separate field is not needed, this can be implemented as a document-scene root sizing policy
that keeps `UISceneNode::mRoot` viewport-sized while the document layout widget carries the scroll
target extent. The important invariant is that setting the scrollable extent must not resize the
root containing block used by normal HTML/body layout.
3. Add a narrow way to disable the nested scene's automatic parent-size following or override root
sizing for embedded document scenes. Preserve the current default for existing nested scenes;
`UIWebView` must prevent automatic parent-size following from turning scroll extent into the root
layout viewport.
4. Make `UISceneNode::getMediaFeatures()` use `getViewportPixelsSize()` for viewport width/height
while keeping device metrics from the window. while keeping device metrics from the window.
4. Audit viewport-relative length conversion. Replace direct `getSceneNode()->getPixelsSize()` use 5. Audit viewport-relative length conversion. Replace direct `getSceneNode()->getPixelsSize()` use
for CSS viewport units with the owning `UISceneNode` viewport metrics. for CSS viewport units with the owning `UISceneNode` viewport metrics.
5. Make HTML/body viewport minimum-height calculations use the document scene viewport metrics. 6. Make HTML/body viewport minimum-height calculations use the document scene viewport metrics.
6. Keep ordinary scene/root sizing and world bounds based on the actual content-sized scene extent. 7. Keep ordinary top-level scene/root sizing unchanged, but for embedded document scenes make root
sizing use the layout viewport while world bounds and scroll target size use the scrollable extent.
8. Document the split close to `UISceneNode::onSizeChange()` or the new root-sizing policy because
future changes to scene sizing can easily reintroduce root-as-scroll-extent behavior.
Tests: Tests:
- A normal scene without a viewport override preserves existing size-based behavior. - A normal scene without a viewport override preserves existing size-based behavior.
- A 3000px-tall scene with a 600px viewport reports 600px media height and resolves `100vh` to - A 3000px-tall scene with a 600px viewport reports 600px media height and resolves `100vh` to
600px. 600px.
- HTML/body minimum height uses the viewport while document/root extent can grow beyond it. - HTML/body minimum height uses the viewport while the document scroll extent can grow beyond it.
- Changing viewport width re-evaluates media queries without forcing the scene extent to viewport - Changing viewport width re-evaluates media queries without forcing the scroll extent to viewport
height. height.
- Growing the scroll extent does not make `html`, `body`, or viewport-width descendants keep a stale
wide containing block after the web view later shrinks.
- Explicit overflow content can grow the scroll extent while `width: 100vw`, `width: 100%`, and root
auto-width layout continue resolving against the layout viewport.
### Phase 2: Harden Embedded UISceneNode Behavior ### Phase 2: Harden Embedded UISceneNode Behavior
@@ -251,38 +297,92 @@ Tests:
Steps: Steps:
1. Add `UISceneNode* mDocumentScene`. 1. Add `UISceneNode* mDocumentScene` and `UILayout* mDocumentLayout` (or a narrow internal
2. Construct the owned scene with the host window, initialize its embedded services, and add it as `DocumentLayout` subclass if hooks are needed).
the `UIWebView` scroll child. Let the normal `UIScrollView` path reparent it into the clipped 2. Construct `mDocumentLayout` as the only direct `UIWebView` scroll child. Let the normal
`mContainer` and select it as the scroll target. `UIScrollView` path reparent it into the clipped `mContainer` and select it as the scroll target.
3. Create `mDocContainer` under `mDocumentScene->getRoot()`, preserving its current vertical, 3. Construct the owned scene with the host window, initialize its embedded services, and parent it
under `mDocumentLayout` so rendering, hit testing, invalidation, and document-scene ownership flow
through the normal tree.
4. Create `mDocContainer` under `mDocumentScene->getRoot()`, preserving its current vertical,
match-width, wrap-content behavior and white initial background. match-width, wrap-content behavior and white initial background.
4. Disable automatic parent-size following for `mDocumentScene`. 5. Disable automatic parent-size following for `mDocumentScene` if following would make scene/root
5. Feed the visible `UIScrollView::mContainer` size into sizing consume the scroll extent. Prefer an explicit embedded-document root sizing policy that
`mDocumentScene->setViewportPixelsSize()`. keeps root layout viewport-sized.
6. Size `mDocumentScene` to at least the viewport width/height and grow it to the laid-out 6. Feed the visible `UIScrollView::mContainer` size into
HTML/body/document extent. Its size is the size observed by `UIScrollView` for scrollbar `mDocumentScene->setViewportPixelsSize()` and the document root/layout viewport sizing API.
calculations. 7. Size `mDocumentLayout` to at least the viewport width/height and grow it to the measured
7. Listen for document extent and visible-container changes so scene extent, viewport metrics, scrollable overflow extent. This size is the size observed by `UIScrollView` for scrollbar
media queries, and scrollbar state are updated in the correct order without layout loops. calculations, but it must not become the root layout containing block.
8. Override `scheduledUpdate()` in `UIWebView`, call the inherited 8. Add a narrow scene flush API that processes dirty styles, dirty style states, and dirty layouts
without running actions, scheduled updates, timers, or arbitrary node updates:
```cpp
void UISceneNode::flushDirtyStyleAndLayout();
```
Internally this should reuse the existing dirty queues and invalidation-depth behavior used by
`UISceneNode::update()`, but stop before `SceneNode::update(elapsed)`. `UIWebView` uses this when
viewport geometry changes need a synchronous style/layout settlement before measuring scroll
extent. Do not call `mDocumentScene->update(Time::Zero)` for this purpose.
9. Add a document-extent dirty flag owned by `UIWebView`, for example:
```cpp
bool mDocumentExtentDirty{ true };
LayoutInvalidationFlags mDocumentExtentDirtyReasons{};
void markDocumentExtentDirty( LayoutInvalidationFlags reasons );
```
Mark it dirty when viewport metrics change, document children are loaded/closed, external CSS
applies, images/fonts/replaced controls change intrinsic size, layout invalidation includes
document/viewport/overflow-affecting reasons, or the scroll target size itself changes.
10. Recompute scrollable extent only when the document-extent dirty flag is set. The recomputation
sequence should be:
- update CSS/layout viewport metrics from `mContainer`,
- if viewport changed, mark root/html/body dirty and invalidate viewport-dependent intrinsic
widths,
- call `mDocumentScene->flushDirtyStyleAndLayout()`,
- measure visible/unclipped scrollable overflow from html/body/document descendants,
- set only `mDocumentLayout` / scroll-target size to the measured extent,
- call `UIScrollView::containerUpdate()` / `updateScroll()` as needed,
- clear the dirty flag only after the measured extent is stable for that pass.
Avoid full-document extent scans from the normal per-frame path when no dirty signal occurred.
11. Listen for document extent and visible-container changes so scroll-target extent, viewport
metrics, media queries, scrollbar state, and root/body layout are updated in the correct order
without layout loops. Treat scrollbar visibility changes as viewport changes only when the scroll
view type makes scrollbars consume viewport space.
12. Override `scheduledUpdate()` in `UIWebView`, call the inherited
`UITouchDraggableWidget::scheduledUpdate()` behavior, and then call `UITouchDraggableWidget::scheduledUpdate()` behavior, and then call
`mDocumentScene->update(elapsed)` exactly once per host-scene frame. `UIScrollView` is already `mDocumentScene->update(elapsed)` exactly once per host-scene frame. `UIScrollView` is already
subscribed through `UITouchDraggableWidget`; do not add a second subscription or add the document subscribed through `UITouchDraggableWidget`; do not add a second subscription or add the document
scene to `SceneManager`. scene to `SceneManager`. If document extent is dirty after the update, run the narrow
9. Expose `getDocumentSceneNode()`. dirty-style/layout flush and extent measurement path; do not run a second full scene update.
10. On web-view scene changes, reinitialize/rebind the document scene's inherited services without 13. Expose `getDocumentSceneNode()`.
14. On web-view scene changes, reinitialize/rebind the document scene's inherited services without
touching document-owned state. touching document-owned state.
Tests: Tests:
- The document scene is attached below the host scene but absent from `SceneManager`. - The document scene is attached below the host scene but absent from `SceneManager`.
- The `UIScrollView` scroll target is `mDocumentLayout`, not `mDocumentScene`.
- `mDocumentScene` is parented below `mDocumentLayout` and remains the scene owner for descendants.
- `mDocContainer`, `html`, `body`, and descendants all belong to the document scene. - `mDocContainer`, `html`, `body`, and descendants all belong to the document scene.
- A page taller than the viewport grows the document scene and produces the correct scroll range. - A page taller than the viewport grows the document layout scroll target and produces the correct
- Scrolling translates the content-sized document scene inside the fixed `UIScrollView` viewport. scroll range.
- Scrolling translates the document layout, including the nested document scene, inside the fixed
`UIScrollView` viewport.
- `vh`/`vw`, viewport media queries, fixed elements, and sticky elements continue using the visible - `vh`/`vw`, viewport media queries, fixed elements, and sticky elements continue using the visible
web-view viewport rather than the document scene extent while the scene is scrolled. web-view viewport rather than the document scroll extent while the document is scrolled.
- Document actions/animations and dirty style/layout queues update through the web view. - Document actions/animations and dirty style/layout queues update through the web view.
- A viewport change can synchronously settle style/layout through `flushDirtyStyleAndLayout()` but
does not run actions, timers, scheduled updates, or arbitrary node update callbacks twice in one
host frame.
- A large stable document does not rescan its full descendant tree every frame when no style, layout,
viewport, image, font, or replaced-control dirty signal occurred.
- After growing to a very wide viewport and then shrinking, root/body/`width:100vw` descendants
resolve against the new viewport, while explicit wide overflow still creates horizontal scroll.
### Phase 4: Route All Document Operations To The Owned Scene ### Phase 4: Route All Document Operations To The Owned Scene
@@ -303,13 +403,17 @@ Steps:
widget. widget.
4. Install the navigation interceptor once during document-scene setup. It must resolve and navigate 4. Install the navigation interceptor once during document-scene setup. It must resolve and navigate
through the owned scene and web view, not replace a host callback on every load. through the owned scene and web view, not replace a host callback on every load.
5. On navigation, close only `mDocContainer` children and remove only nonpersistent rules from the 5. On navigation, close only `mDocContainer` children, remove only nonpersistent rules from the
document scene. document scene, clear document-local author font aliases/resources from the previous document, and
mark document extent dirty.
6. Update the HTML example's injected Hacker News stylesheet to combine into 6. Update the HTML example's injected Hacker News stylesheet to combine into
`webView->getDocumentSceneNode()`. `webView->getDocumentSceneNode()`.
7. Audit document descendants that use `getUISceneNode()` for links, forms, images, inline styles, 7. Audit document descendants that use `getUISceneNode()` for links, forms, images, inline styles,
external CSS, fonts, and relative URLs. They should work without special cases once parentage is external CSS, fonts, and relative URLs. They should work without special cases once parentage is
correct. correct.
8. Ensure any document-scoped style injection, including application-provided user CSS, calls the
document-scene style API and then marks the owning `UIWebView` document extent dirty when layout
may be affected.
Tests: Tests:
@@ -323,6 +427,10 @@ Tests:
correct document URI. correct document URI.
- Application-level `findByType(UI_TYPE_HTML_HTML)` no longer returns a web-view document node; - Application-level `findByType(UI_TYPE_HTML_HTML)` no longer returns a web-view document node;
document-scene lookup does. document-scene lookup does.
- Two web views can load external CSS files with the same selectors and relative URLs without either
stylesheet, URL base, or late style application leaking into the other document.
- Injecting document CSS through `getDocumentSceneNode()->combineStyleSheet(...)` relayouts and
remeasures the document extent, while injecting host CSS does not affect document descendants.
### Phase 5: Isolate Author Font Faces ### Phase 5: Isolate Author Font Faces
@@ -343,14 +451,23 @@ Steps:
3. Register author fonts under an internal scene-unique resource name if `FontManager` registration 3. Register author fonts under an internal scene-unique resource name if `FontManager` registration
remains required, while preserving the author-visible family only in the scene-local alias. remains required, while preserving the author-visible family only in the scene-local alias.
4. Keep generic/system fonts and explicitly shared application defaults as global fallbacks. 4. Keep generic/system fonts and explicitly shared application defaults as global fallbacks.
5. Remove only the scene's internally registered author fonts during scene destruction. 5. Add an explicit `clearDocumentFontFaces()` / `clearAuthorFontFaces()` operation used during
6. Apply the async scene-lifetime guard from the next phase to remote `@font-face` loads. navigation before new document CSS is loaded. It removes only this scene's internally registered
author fonts and clears aliases; it must not remove application/system fonts or sibling-document
author fonts.
6. Remove any remaining scene-owned author fonts during scene destruction.
7. Apply the async scene-lifetime guard from the next phase to remote, local deferred, and VFS
`@font-face` loads.
8. Mark the owning web-view document extent dirty after a loaded font is registered because font
metrics can change line wrapping, table sizing, and scroll extent.
Tests: Tests:
- Two web views can declare the same family name with different font files and each resolves its own - Two web views can declare the same family name with different font files and each resolves its own
font. font.
- Destroying or navigating one web view does not remove or replace the sibling's author font. - Destroying or navigating one web view does not remove or replace the sibling's author font.
- Navigating one web view clears its previous document-local author font alias and replaces it with
the new document's face without leaving the old alias visible to CSS.
- Application/system font lookup still works when no document-local face matches. - Application/system font lookup still works when no document-local face matches.
### Phase 6: Make Navigation And Resource Loading Lifetime-Safe ### Phase 6: Make Navigation And Resource Loading Lifetime-Safe
@@ -363,23 +480,41 @@ Tests:
- `src/eepp/ui/uiscenenode.cpp` - `src/eepp/ui/uiscenenode.cpp`
- `src/tests/unit_tests/uiwebview_tests.cpp` - `src/tests/unit_tests/uiwebview_tests.cpp`
The current async HTTP callbacks capture raw `this` and a raw scene pointer. Owning a scene makes The current async/deferred callbacks can capture raw `this` and raw scene pointers. Owning a scene
correct cancellation and stale-response handling part of the document lifecycle. The same audit makes correct cancellation and stale-response handling part of the document lifecycle. The audit must
must cover async document resources such as `UISceneNode::loadCSS()`, which currently captures the cover every deferred document resource path, not only HTTP:
scene directly.
- top-level HTTP document navigation,
- HTTP redirects and cookies,
- external HTTP CSS,
- deferred local-file CSS through the thread pool,
- VFS CSS if it can become deferred later,
- HTTP, local-file, data URI, and VFS `@font-face`,
- async image/replaced-resource loads,
- main-thread reposts created by any of the above.
Steps: Steps:
1. Add a small shared navigation/load state containing an alive owner pointer and monotonically 1. Add a small shared navigation/load state containing an alive owner pointer and monotonically
increasing navigation generation. increasing navigation generation.
2. Async callbacks capture a weak load state, not raw `this` or raw scene pointers. 2. Async top-level navigation callbacks capture a weak load state, not raw `this` or raw scene
3. Before storing cookies, posting to the main thread, or applying a response, verify that the web pointers.
view still exists and the generation is current. 3. Add a scene-local async resource state for document subresources. It must be invalidated on
4. A newer navigation invalidates older responses. Destruction invalidates all pending callbacks. navigation, scene destruction, and explicit document resource reset.
5. Keep history mutation and navigation events deterministic when requests fail or are superseded. 4. Subresource callbacks capture the scene-local state and a generation. They may capture immutable
6. Give document-scene async resource loads a scene-lifetime guard before they enqueue main-thread values such as resolved URLs and parsed buffers, but must not dereference the scene or widget
until the generation/alive check has passed on the main thread.
5. Before storing cookies, posting to the main thread, applying a response, combining a stylesheet,
registering a font, mutating an image/replaced widget, or marking document extent dirty, verify
that the owner still exists and the generation is current.
6. A newer navigation invalidates older document responses and all old document subresources.
Destruction invalidates all pending callbacks.
7. Keep history mutation and navigation events deterministic when requests fail or are superseded.
8. Give document-scene async resource loads a scene-lifetime guard before they enqueue main-thread
work or mutate the stylesheet. Audit other document-triggered async loaders and use the same work or mutate the stylesheet. Audit other document-triggered async loaders and use the same
pattern where they retain scene/widget pointers. pattern where they retain scene/widget pointers.
9. When a stale callback is ignored, it must not store cookies, mutate style/font/image state, mark
extent dirty, send navigation events, or trigger layout.
Tests: Tests:
@@ -387,6 +522,11 @@ Tests:
- A slow old response cannot replace a newer document. - A slow old response cannot replace a newer document.
- Stale redirects/cookies do not mutate the current document scene. - Stale redirects/cookies do not mutate the current document scene.
- Destroying a web view while external CSS is loading is safe and cannot mutate another scene. - Destroying a web view while external CSS is loading is safe and cannot mutate another scene.
- Deferred local CSS that completes after navigation/destruction is ignored safely.
- A remote or deferred `@font-face` response from an old navigation cannot register a font alias,
replace the current document's font, or remeasure the current document.
- An async image/replaced-resource response from an old navigation cannot mutate a closed widget or
dirty the current document extent.
### Phase 7: Integration And Documentation ### Phase 7: Integration And Documentation
@@ -416,7 +556,7 @@ Steps:
| `src/eepp/ui/uinode.cpp` | Resolve viewport units from document viewport instead of scene extent | | `src/eepp/ui/uinode.cpp` | Resolve viewport units from document viewport instead of scene extent |
| `src/eepp/ui/uirichtext.cpp` | HTML/body viewport minimum-height handling | | `src/eepp/ui/uirichtext.cpp` | HTML/body viewport minimum-height handling |
| `include/eepp/ui/uiwebview.hpp` | Owned scene, getter, scheduled update, lifetime state | | `include/eepp/ui/uiwebview.hpp` | Owned scene, getter, scheduled update, lifetime state |
| `src/eepp/ui/uiwebview.cpp` | Content-sized scene scroll target, viewport updates, isolated loading/navigation | | `src/eepp/ui/uiwebview.cpp` | Document layout scroll target, viewport updates, isolated loading/navigation |
| `src/examples/ui_html/ui_html.cpp` | Inject document CSS through the document scene | | `src/examples/ui_html/ui_html.cpp` | Inject document CSS through the document scene |
| `src/tests/unit_tests/uiwebview_tests.cpp` | New focused isolation/lifecycle tests | | `src/tests/unit_tests/uiwebview_tests.cpp` | New focused isolation/lifecycle tests |
| `src/tests/unit_tests/uihtml_tests.cpp` | Existing realistic web-view fixture adjustments | | `src/tests/unit_tests/uihtml_tests.cpp` | Existing realistic web-view fixture adjustments |
@@ -435,9 +575,9 @@ media state, dirty queues, cookies, font faces, or DOM lookup. This does not cre
### Keep UISceneNode Size As Both Document Extent And CSS Viewport ### Keep UISceneNode Size As Both Document Extent And CSS Viewport
The document scene must be content-sized to scroll, but that size cannot also define viewport units The document needs a scrollable extent, but that size cannot also define viewport units, media
and media queries. Keep one content-sized scene and add explicit viewport metrics instead of adding queries, or the root/body containing block. Keep a real document layout scroll target and explicit
an extra nested scroll target or treating the content extent as the viewport. viewport/layout-viewport metrics instead of treating content extent as the viewport.
### Add Every Document Scene To SceneManager ### Add Every Document Scene To SceneManager
+28
View File
@@ -118,6 +118,20 @@ class EE_API UISceneNode : public SceneNode {
/** @return The explicit viewport size, or the scene extent when no override is set. */ /** @return The explicit viewport size, or the scene extent when no override is set. */
const Sizef& getViewportPixelsSize() const; const Sizef& getViewportPixelsSize() const;
/**
* @brief Sets the layout viewport size used by the scene root as the initial containing block.
*
* Embedded document scenes can have a scroll extent larger than the CSS/layout viewport. This
* keeps root/body normal-flow layout viewport-sized while the scroll target owns overflow.
*/
void setLayoutViewportPixelsSize( const Sizef& size );
/** Clears the explicit layout viewport size so the CSS viewport is used. */
void clearLayoutViewportPixelsSize();
/** @return The explicit layout viewport size, or the CSS viewport when no override is set. */
const Sizef& getLayoutViewportPixelsSize() const;
/** /**
* @brief Controls whether a nested scene automatically follows its direct parent's size. * @brief Controls whether a nested scene automatically follows its direct parent's size.
* *
@@ -157,6 +171,15 @@ class EE_API UISceneNode : public SceneNode {
*/ */
virtual void update( const Time& elapsed ); virtual void update( const Time& elapsed );
/**
* @brief Flushes dirty styles, style states, and layouts without running scene/node updates.
*
* This is intended for embedded document scenes that need synchronous style/layout settlement
* before measuring scroll extent. It must not run actions, timers, scheduled updates, or
* arbitrary node update callbacks.
*/
void flushDirtyStyleAndLayout();
/** /**
* @brief Sets the translator for internationalization. * @brief Sets the translator for internationalization.
* *
@@ -692,6 +715,8 @@ class EE_API UISceneNode : public SceneNode {
*/ */
void nodeToWorldTranslation( Vector2f& Pos ) const; void nodeToWorldTranslation( Vector2f& Pos ) const;
Node* overFind( const Vector2f& point );
/** /**
* @brief Reloads the UI styles. * @brief Reloads the UI styles.
* *
@@ -846,6 +871,8 @@ class EE_API UISceneNode : public SceneNode {
Uint32 mCurrentMarker{ 0 }; Uint32 mCurrentMarker{ 0 };
Sizef mViewportPixelsSize; Sizef mViewportPixelsSize;
bool mHasViewportPixelsSize{ false }; bool mHasViewportPixelsSize{ false };
Sizef mLayoutViewportPixelsSize;
bool mHasLayoutViewportPixelsSize{ false };
bool mFollowParentSize{ true }; bool mFollowParentSize{ true };
bool mOwnsEventDispatcher{ true }; bool mOwnsEventDispatcher{ true };
std::shared_ptr<ThreadPool> mThreadPool; std::shared_ptr<ThreadPool> mThreadPool;
@@ -900,6 +927,7 @@ class EE_API UISceneNode : public SceneNode {
void updateParentSizeListener(); void updateParentSizeListener();
void onViewportPixelsSizeChange(); void onViewportPixelsSizeChange();
const Sizef& getRootPixelsSize() const;
UISceneNode* getHostUISceneNode() const; UISceneNode* getHostUISceneNode() const;
/** /**
+6 -3
View File
@@ -19,6 +19,7 @@ namespace EE { namespace UI {
class UIHTMLHtml; class UIHTMLHtml;
class UIHTMLBody; class UIHTMLBody;
class UILayout;
class UISceneNode; class UISceneNode;
class EE_API UIWebView : public UIScrollView { class EE_API UIWebView : public UIScrollView {
@@ -95,13 +96,15 @@ class EE_API UIWebView : public UIScrollView {
UIWebView(); UIWebView();
UISceneNode* mDocumentScene{ nullptr }; UISceneNode* mDocumentScene{ nullptr };
UILayout* mDocumentLayout{ nullptr };
UIWidget* mDocContainer{ nullptr }; UIWidget* mDocContainer{ nullptr };
Uint32 mScrollContainerSizeChangeCb{ 0 }; Uint32 mScrollContainerSizeChangeCb{ 0 };
Uint32 mVerticalScrollVisibleChangeCb{ 0 }; Uint32 mVerticalScrollVisibleChangeCb{ 0 };
Uint32 mHorizontalScrollVisibleChangeCb{ 0 }; Uint32 mHorizontalScrollVisibleChangeCb{ 0 };
bool mUpdatingDocumentViewportMetrics{ false }; bool mUpdatingDocumentViewportMetrics{ false };
bool mUpdatingDocumentContentExtent{ false }; bool mUpdatingDocumentContentExtent{ false };
bool mDocumentViewportSyncQueued{ false }; bool mDocumentExtentDirty{ true };
LayoutInvalidationFlags mDocumentExtentDirtyReasons{ 0 };
struct NavigationLoadState { struct NavigationLoadState {
UIWebView* owner{ nullptr }; UIWebView* owner{ nullptr };
bool alive{ true }; bool alive{ true };
@@ -142,9 +145,9 @@ class EE_API UIWebView : public UIScrollView {
void onDocumentViewportGeometryChanged(); void onDocumentViewportGeometryChanged();
void updateHTMLMinHeight( UIHTMLHtml* html, UIHTMLBody* body ); void updateHTMLMinHeight( UIHTMLHtml* html, UIHTMLBody* body );
void updateHTMLMinHeightForDocument(); void updateHTMLMinHeightForDocument();
void markDocumentExtentDirty( LayoutInvalidationFlags reasons );
bool updateDocumentViewportMetrics(); bool updateDocumentViewportMetrics();
void updateDocumentSceneContentExtent(); void updateDocumentMetricsIfNeeded();
void updateDocumentSceneMetrics();
}; };
}} // namespace EE::UI }} // namespace EE::UI
+104 -3
View File
@@ -47,6 +47,17 @@ static void refreshWebViewDocumentLayoutAfterStyleChange( UIWidget* root ) {
// viewport did not move. Keeping it scoped to WebView documents avoids reopening the generic // viewport did not move. Keeping it scoped to WebView documents avoids reopening the generic
// RichText parent-recompute storm, while the final html dirty mark gives the normal layout // RichText parent-recompute storm, while the final html dirty mark gives the normal layout
// queue one coalesced pass from the document root. // queue one coalesced pass from the document root.
Node* parent = root;
while ( parent ) {
if ( parent->isType( UI_TYPE_WEBVIEW ) ) {
parent->asType<UIWebView>()->invalidateDocumentLayout(
LayoutInvalidation::Document |
toLayoutInvalidationFlags( LayoutInvalidationReason::Style ) );
break;
}
parent = parent->getParent();
}
auto webViews = root->findAllByType( UI_TYPE_WEBVIEW ); auto webViews = root->findAllByType( UI_TYPE_WEBVIEW );
for ( auto webViewNode : webViews ) { for ( auto webViewNode : webViews ) {
auto* webView = webViewNode->asType<UIWebView>(); auto* webView = webViewNode->asType<UIWebView>();
@@ -168,6 +179,54 @@ void UISceneNode::nodeToWorldTranslation( Vector2f& Pos ) const {
} }
} }
Node* UISceneNode::overFind( const Vector2f& point ) {
Node* pOver = NULL;
if ( ( mNodeFlags & NODE_FLAG_OVER_FIND_ALLOWED ) && mEnabled && mVisible ) {
updateWorldPolygon();
if ( mWorldBounds.contains( point ) && mPoly.pointInside( point ) ) {
writeNodeFlag( NODE_FLAG_MOUSEOVER_ME_OR_CHILD, 1 );
mSceneNode->addMouseOverNode( this );
Node* child = getLastChild();
while ( NULL != child ) {
Node* childOver = child->overFind( point );
if ( NULL == childOver && child == mRoot && mHasLayoutViewportPixelsSize ) {
mRoot->updateWorldPolygon();
mRoot->writeNodeFlag( NODE_FLAG_MOUSEOVER_ME_OR_CHILD, 1 );
mSceneNode->addMouseOverNode( mRoot );
Node* rootChild = mRoot->getLastChild();
while ( NULL != rootChild ) {
childOver = rootChild->overFind( point );
if ( NULL != childOver )
break;
rootChild = rootChild->getPrevNode();
}
if ( NULL == childOver )
childOver = mRoot;
}
if ( NULL != childOver ) {
pOver = childOver;
break;
}
child = child->getPrevNode();
}
if ( NULL == pOver )
pOver = this;
}
}
return pOver;
}
void UISceneNode::onParentChange() { void UISceneNode::onParentChange() {
SceneNode::onParentChange(); SceneNode::onParentChange();
@@ -789,7 +848,7 @@ void UISceneNode::setViewportPixelsSize( const Sizef& size ) {
mViewportPixelsSize = size; mViewportPixelsSize = size;
mHasViewportPixelsSize = true; mHasViewportPixelsSize = true;
mRoot->setPixelsSize( getPixelsSize() ); mRoot->setPixelsSize( getRootPixelsSize() );
onViewportPixelsSizeChange(); onViewportPixelsSizeChange();
} }
@@ -798,7 +857,7 @@ void UISceneNode::clearViewportPixelsSize() {
return; return;
mHasViewportPixelsSize = false; mHasViewportPixelsSize = false;
mRoot->setPixelsSize( getPixelsSize() ); mRoot->setPixelsSize( getRootPixelsSize() );
onViewportPixelsSizeChange(); onViewportPixelsSizeChange();
} }
@@ -812,6 +871,29 @@ const Sizef& UISceneNode::getViewportPixelsSize() const {
return mHasViewportPixelsSize ? mViewportPixelsSize : getPixelsSize(); return mHasViewportPixelsSize ? mViewportPixelsSize : getPixelsSize();
} }
void UISceneNode::setLayoutViewportPixelsSize( const Sizef& size ) {
if ( mHasLayoutViewportPixelsSize && mLayoutViewportPixelsSize == size )
return;
mLayoutViewportPixelsSize = size;
mHasLayoutViewportPixelsSize = true;
mRoot->setPixelsSize( getRootPixelsSize() );
sendMsg( this, NodeMessage::WindowResize );
}
void UISceneNode::clearLayoutViewportPixelsSize() {
if ( !mHasLayoutViewportPixelsSize )
return;
mHasLayoutViewportPixelsSize = false;
mRoot->setPixelsSize( getRootPixelsSize() );
sendMsg( this, NodeMessage::WindowResize );
}
const Sizef& UISceneNode::getLayoutViewportPixelsSize() const {
return mHasLayoutViewportPixelsSize ? mLayoutViewportPixelsSize : getViewportPixelsSize();
}
void UISceneNode::setFollowParentSize( bool followParentSize ) { void UISceneNode::setFollowParentSize( bool followParentSize ) {
if ( mFollowParentSize == followParentSize ) if ( mFollowParentSize == followParentSize )
return; return;
@@ -828,6 +910,21 @@ bool UISceneNode::followsParentSize() const {
return mFollowParentSize; return mFollowParentSize;
} }
void UISceneNode::flushDirtyStyleAndLayout() {
updateDirtyStyles();
updateDirtyStyleStates();
updateDirtyLayouts();
int invalidationDepth = mMaxInvalidationDepth;
while ( ( !mDirtyStyle.empty() || !mDirtyStyleState.empty() || !mDirtyLayouts.empty() ) &&
invalidationDepth > 0 ) {
updateDirtyStyles();
updateDirtyStyleStates();
updateDirtyLayouts();
invalidationDepth--;
}
}
void UISceneNode::update( const Time& elapsed ) { void UISceneNode::update( const Time& elapsed ) {
UISceneNode* uiSceneNode = SceneManager::instance()->getUISceneNode(); UISceneNode* uiSceneNode = SceneManager::instance()->getUISceneNode();
@@ -1196,7 +1293,11 @@ void UISceneNode::onChildCountChange( Node* child, const bool& removed ) {
void UISceneNode::onSizeChange() { void UISceneNode::onSizeChange() {
SceneNode::onSizeChange(); SceneNode::onSizeChange();
mRoot->setPixelsSize( getPixelsSize() ); mRoot->setPixelsSize( getRootPixelsSize() );
}
const Sizef& UISceneNode::getRootPixelsSize() const {
return mHasLayoutViewportPixelsSize ? mLayoutViewportPixelsSize : getViewportPixelsSize();
} }
void UISceneNode::processStyleSheetAtRules( const StyleSheet& styleSheet, URI baseURI ) { void UISceneNode::processStyleSheetAtRules( const StyleSheet& styleSheet, URI baseURI ) {
+107 -126
View File
@@ -13,6 +13,16 @@
namespace EE { namespace UI { namespace EE { namespace UI {
class UIWebViewDocumentContainer : public UILinearLayout {
public:
static UIWebViewDocumentContainer* New() { return eeNew( UIWebViewDocumentContainer, () ); }
void clearDocumentChildren() { childDeleteAll(); }
protected:
UIWebViewDocumentContainer() : UILinearLayout( "webview::doc", UIOrientation::Vertical ) {}
};
static void expandDocumentContentExtent( Node* node, Vector2f offset, Sizef& extent, static void expandDocumentContentExtent( Node* node, Vector2f offset, Sizef& extent,
bool hasClip = false, bool hasClip = false,
const Sizef& clipExtent = Sizef::Zero ) { const Sizef& clipExtent = Sizef::Zero ) {
@@ -55,7 +65,6 @@ static void expandDocumentContentExtent( Node* node, Vector2f offset, Sizef& ext
child = child->getNextNode(); child = child->getNextNode();
} }
} }
static void expandWidgetContentExtent( UIWidget* widget, const Vector2f& offset, Sizef& extent ) { static void expandWidgetContentExtent( UIWidget* widget, const Vector2f& offset, Sizef& extent ) {
if ( !widget || widget->isClosing() ) if ( !widget || widget->isClosing() )
return; return;
@@ -84,9 +93,6 @@ static void resetViewportDependentDocumentWidths( UIWidget* container ) {
if ( !parentWidget ) if ( !parentWidget )
continue; continue;
const Float containerWidth = eemax( 0.f, parentWidget->getPixelsSize().getWidth() -
parentWidget->getPixelsContentOffset().Left -
parentWidget->getPixelsContentOffset().Right );
bool normalFlow = true; bool normalFlow = true;
Rectf margin = widget->getLayoutPixelsMargin(); Rectf margin = widget->getLayoutPixelsMargin();
if ( widget->isType( UI_TYPE_HTML_WIDGET ) ) { if ( widget->isType( UI_TYPE_HTML_WIDGET ) ) {
@@ -97,29 +103,15 @@ static void resetViewportDependentDocumentWidths( UIWidget* container ) {
if ( normalFlow ) { if ( normalFlow ) {
widget->invalidateIntrinsicSize(); widget->invalidateIntrinsicSize();
/* if ( widget->getLayoutWidthPolicy() == SizePolicy::MatchParent ) {
widget->setPixelsSize( eemax( 0.f, containerWidth - margin.Left - margin.Right ),
widget->getPixelsSize().getHeight() );
} else if ( widget->getLayoutWidthPolicy() == SizePolicy::Fixed &&
widget->getUIStyle() ) {
const StyleSheetProperty* wprop =
widget->getUIStyle()->getProperty( PropertyId::Width );
if ( wprop && StyleSheetLength::isPercentage( wprop->value() ) ) {
widget->setPixelsSize( { widget->lengthFromValue( *wprop ),
widget->getPixelsSize().getHeight() } );
}
} */
} }
} }
} }
Sizef UIWebView::getDocumentViewportPixelsSize() const { Sizef UIWebView::getDocumentViewportPixelsSize() const {
Sizef contentBox = getPixelsSize(); Sizef viewport = getPixelsSize();
const Rectf& padding = getPixelsPadding(); const Rectf& padding = getPixelsPadding();
contentBox.x -= padding.Left + padding.Right; viewport.x -= padding.Left + padding.Right;
contentBox.y -= padding.Top + padding.Bottom; viewport.y -= padding.Top + padding.Bottom;
Sizef viewport = contentBox;
if ( getViewType() == ScrollViewType::Outside ) { if ( getViewType() == ScrollViewType::Outside ) {
if ( getVerticalScrollBar()->isVisible() ) if ( getVerticalScrollBar()->isVisible() )
@@ -128,24 +120,6 @@ Sizef UIWebView::getDocumentViewportPixelsSize() const {
viewport.y -= getHorizontalScrollBar()->getPixelsSize().getHeight(); viewport.y -= getHorizontalScrollBar()->getPixelsSize().getHeight();
} }
if ( mContainer && mContainer->getPixelsSize() != Sizef::Zero ) {
Sizef container = mContainer->getPixelsSize();
Float verticalScrollWidth = getVerticalScrollBar()->getPixelsSize().getWidth();
Float horizontalScrollHeight = getHorizontalScrollBar()->getPixelsSize().getHeight();
if ( eeabs( container.getWidth() - viewport.getWidth() ) <= 0.5f ||
( getVerticalScrollBar()->isVisible() &&
container.getWidth() < contentBox.getWidth() &&
eeabs( contentBox.getWidth() - container.getWidth() - verticalScrollWidth ) <=
0.5f ) )
viewport.x = container.getWidth();
if ( eeabs( container.getHeight() - viewport.getHeight() ) <= 0.5f ||
( getHorizontalScrollBar()->isVisible() &&
container.getHeight() < contentBox.getHeight() &&
eeabs( contentBox.getHeight() - container.getHeight() - horizontalScrollHeight ) <=
0.5f ) )
viewport.y = container.getHeight();
}
viewport.x = eemax( 0.f, viewport.x ); viewport.x = eemax( 0.f, viewport.x );
viewport.y = eemax( 0.f, viewport.y ); viewport.y = eemax( 0.f, viewport.y );
return viewport; return viewport;
@@ -158,11 +132,18 @@ UIWebView* UIWebView::New() {
UIWebView::UIWebView() : UIScrollView( "webview" ) { UIWebView::UIWebView() : UIScrollView( "webview" ) {
mNavigationLoadState = std::make_shared<NavigationLoadState>(); mNavigationLoadState = std::make_shared<NavigationLoadState>();
mNavigationLoadState->owner = this; mNavigationLoadState->owner = this;
mDocumentLayout = UILinearLayout::NewVerticalWidthMatchParent( "webview::document_layout" );
mDocumentLayout->setClipType( ClipType::None );
mDocumentLayout->setFlags( UI_OWNS_CHILDREN_POSITION );
mDocumentLayout->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
mDocumentLayout->setParent( this );
mDocumentScene = UISceneNode::New(); mDocumentScene = UISceneNode::New();
mDocumentScene->setFollowParentSize( false ); mDocumentScene->setFollowParentSize( false );
mDocumentScene->setParent( this ); mDocumentScene->setParent( mDocumentLayout );
mDocContainer = UILinearLayout::NewVerticalWidthMatchParent( "webview::doc" ); mDocContainer = UIWebViewDocumentContainer::New();
mDocContainer->setClipType( ClipType::None ); mDocContainer->setClipType( ClipType::None );
mDocContainer->setFlags( UI_OWNS_CHILDREN_POSITION ); mDocContainer->setFlags( UI_OWNS_CHILDREN_POSITION );
mDocContainer->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ); mDocContainer->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent );
@@ -223,7 +204,7 @@ void UIWebView::scheduledUpdate( const Time& time ) {
UITouchDraggableWidget::scheduledUpdate( time ); UITouchDraggableWidget::scheduledUpdate( time );
if ( mDocumentScene ) { if ( mDocumentScene ) {
mDocumentScene->update( time ); mDocumentScene->update( time );
updateDocumentSceneContentExtent(); updateDocumentMetricsIfNeeded();
} }
} }
@@ -378,67 +359,53 @@ void UIWebView::loadDocumentData( URI url, std::string data, Uint64 generation )
} }
std::weak_ptr<NavigationLoadState> loadState( mNavigationLoadState ); std::weak_ptr<NavigationLoadState> loadState( mNavigationLoadState );
ensureMainThread( ensureMainThread( [loadState, generation, url = std::move( url ),
[loadState, generation, url = std::move( url ), data = std::move( data )]() mutable { data = std::move( data )]() mutable {
UIWebView* self = resolveNavigationLoad( loadState, generation ); UIWebView* self = resolveNavigationLoad( loadState, generation );
if ( !self ) if ( !self )
return; return;
auto ui = self->getDocumentSceneNode(); auto ui = self->getDocumentSceneNode();
if ( !ui || !self->isNavigationLoadCurrent( generation ) ) if ( !ui || !self->isNavigationLoadCurrent( generation ) )
return; return;
self->getVerticalScrollBar()->setValue( 0 ); self->getVerticalScrollBar()->setValue( 0 );
self->mDocContainer->closeAllChildren(); self->getHorizontalScrollBar()->setValue( 0 );
ui->invalidateAsyncResourceLoads(); static_cast<UIWebViewDocumentContainer*>( self->mDocContainer )->clearDocumentChildren();
ui->getStyleSheet().removeAllWithoutMarker( self->mStyleSheetDefaultMarker ); ui->invalidateAsyncResourceLoads();
ui->setURIFromURL( url ); ui->getStyleSheet().removeAllWithoutMarker( self->mStyleSheetDefaultMarker );
ui->setURIFromURL( url );
auto hash = String::hash( url.toString() ); auto hash = String::hash( url.toString() );
ui->loadLayoutFromString( Tools::HTMLFormatter::HTMLtoXML( data ), self->mDocContainer, ui->loadLayoutFromString( Tools::HTMLFormatter::HTMLtoXML( data ), self->mDocContainer,
hash ); hash );
ui->setNavigationInterceptorCb( [loadState]( const NavigationRequest& request ) { ui->setNavigationInterceptorCb( [loadState]( const NavigationRequest& request ) {
auto locked = loadState.lock(); auto locked = loadState.lock();
if ( !locked || !locked->alive || locked->owner == nullptr ) if ( !locked || !locked->alive || locked->owner == nullptr )
return true;
UIWebView* self = locked->owner;
UISceneNode* docScene = self->getDocumentSceneNode();
if ( !docScene )
return true;
URI uri = docScene->solveRelativePath( request.uri );
self->loadURI( uri, request.method != "GET", request.method, request.body,
request.extraHeaders );
return true; return true;
} ); UIWebView* self = locked->owner;
UISceneNode* docScene = self->getDocumentSceneNode();
if ( !self->isNavigationLoadCurrent( generation ) ) if ( !docScene )
return; return true;
self->mIsLoading = false; URI uri = docScene->solveRelativePath( request.uri );
NavigationEvent ev( self, (Uint32)Event::OnNavigationCompleted, url, true ); self->loadURI( uri, request.method != "GET", request.method, request.body,
self->sendEvent( &ev ); request.extraHeaders );
self->updateDocumentViewportMetrics(); return true;
self->updateHTMLMinHeightForDocument();
self->updateDocumentSceneContentExtent();
} ); } );
if ( !self->isNavigationLoadCurrent( generation ) )
return;
self->mIsLoading = false;
NavigationEvent ev( self, (Uint32)Event::OnNavigationCompleted, url, true );
self->sendEvent( &ev );
self->markDocumentExtentDirty( LayoutInvalidation::Document );
self->updateDocumentMetricsIfNeeded();
} );
} }
void UIWebView::onDocumentViewportGeometryChanged() { void UIWebView::onDocumentViewportGeometryChanged() {
if ( updateDocumentViewportMetrics() ) { markDocumentExtentDirty( toLayoutInvalidationFlags( LayoutInvalidationReason::Viewport ) );
updateHTMLMinHeightForDocument();
if ( mDocContainer && mDocContainer->isLayout() )
mDocContainer->asType<UILayout>()->setLayoutDirty( LayoutInvalidation::Document );
if ( auto htmlNode = mDocumentScene->findByType( UI_TYPE_HTML_HTML ) ) {
if ( htmlNode->isWidget() )
resetViewportDependentDocumentWidths( htmlNode->asType<UIWidget>() );
if ( htmlNode->isLayout() )
htmlNode->asType<UILayout>()->setLayoutDirty( LayoutInvalidation::Document );
}
mDocumentScene->update( Time::Zero );
updateDocumentSceneContentExtent();
return;
}
updateDocumentSceneContentExtent();
} }
void UIWebView::updateHTMLMinHeightForDocument() { void UIWebView::updateHTMLMinHeightForDocument() {
@@ -516,7 +483,7 @@ UISceneNode* UIWebView::getDocumentSceneNode() const {
} }
bool UIWebView::updateDocumentViewportMetrics() { bool UIWebView::updateDocumentViewportMetrics() {
if ( !mDocumentScene || !mDocContainer || mUpdatingDocumentViewportMetrics ) if ( !mDocumentScene || !mDocumentLayout || !mDocContainer || mUpdatingDocumentViewportMetrics )
return false; return false;
mUpdatingDocumentViewportMetrics = true; mUpdatingDocumentViewportMetrics = true;
@@ -524,34 +491,63 @@ bool UIWebView::updateDocumentViewportMetrics() {
Sizef viewport = getDocumentViewportPixelsSize(); Sizef viewport = getDocumentViewportPixelsSize();
bool changed = viewport != Sizef::Zero && viewport != mDocumentScene->getViewportPixelsSize(); bool changed = viewport != Sizef::Zero && viewport != mDocumentScene->getViewportPixelsSize();
if ( viewport != Sizef::Zero ) if ( viewport != Sizef::Zero ) {
mDocumentScene->setViewportPixelsSize( viewport ); mDocumentScene->setViewportPixelsSize( viewport );
mDocumentScene->setLayoutViewportPixelsSize( viewport );
}
if ( changed && !mUpdatingDocumentContentExtent && viewport != mDocumentScene->getPixelsSize() ) if ( mDocumentLayout->getPixelsSize() == Sizef::Zero && viewport != Sizef::Zero )
mDocumentScene->setPixelsSize( viewport ); mDocumentLayout->setPixelsSize( viewport );
mUpdatingDocumentViewportMetrics = false; mUpdatingDocumentViewportMetrics = false;
return changed; return changed;
} }
void UIWebView::updateDocumentSceneContentExtent() { void UIWebView::markDocumentExtentDirty( LayoutInvalidationFlags reasons ) {
if ( !mDocumentScene || !mDocContainer || mUpdatingDocumentContentExtent ) mDocumentExtentDirty = true;
mDocumentExtentDirtyReasons |= reasons;
}
void UIWebView::updateDocumentMetricsIfNeeded() {
if ( !mDocumentScene || !mDocumentLayout || !mDocContainer || mUpdatingDocumentContentExtent ||
!mDocumentExtentDirty )
return; return;
mUpdatingDocumentContentExtent = true; mUpdatingDocumentContentExtent = true;
if ( !mUpdatingDocumentViewportMetrics ) bool viewportChanged = !mUpdatingDocumentViewportMetrics && updateDocumentViewportMetrics();
updateDocumentViewportMetrics();
Sizef viewport = mDocumentScene->getViewportPixelsSize(); Sizef viewport = mDocumentScene->getViewportPixelsSize();
if ( viewport == Sizef::Zero ) if ( viewport == Sizef::Zero )
viewport = mContainer ? mContainer->getPixelsSize() : getPixelsSize(); viewport = mContainer ? mContainer->getPixelsSize() : getPixelsSize();
const bool documentReset =
mDocumentExtentDirtyReasons &
toLayoutInvalidationFlags( LayoutInvalidationReason::DocumentExtent );
if ( !viewportChanged && !documentReset ) {
mDocumentExtentDirty = false;
mDocumentExtentDirtyReasons = 0;
mUpdatingDocumentContentExtent = false;
return;
}
if ( viewportChanged || documentReset ) {
updateHTMLMinHeightForDocument();
if ( mDocContainer->isLayout() )
mDocContainer->asType<UILayout>()->setLayoutDirty( LayoutInvalidation::Document );
if ( auto htmlNode = mDocumentScene->findByType( UI_TYPE_HTML_HTML ) ) {
if ( htmlNode->isWidget() )
resetViewportDependentDocumentWidths( htmlNode->asType<UIWidget>() );
if ( htmlNode->isLayout() )
htmlNode->asType<UILayout>()->setLayoutDirty( LayoutInvalidation::Document );
}
}
mDocumentScene->flushDirtyStyleAndLayout();
Sizef extent( viewport.getWidth(), viewport.getHeight() ); Sizef extent( viewport.getWidth(), viewport.getHeight() );
if ( auto htmlNode = mDocContainer->findByType( UI_TYPE_HTML_HTML ) ) { if ( auto htmlNode = mDocContainer->findByType( UI_TYPE_HTML_HTML ) ) {
UIWidget* html = htmlNode->asType<UIWidget>(); UIWidget* html = htmlNode->asType<UIWidget>();
resetViewportDependentDocumentWidths( html );
mDocumentScene->updateDirtyLayouts();
expandWidgetContentExtent( html, Vector2f::Zero, extent ); expandWidgetContentExtent( html, Vector2f::Zero, extent );
} }
@@ -562,31 +558,17 @@ void UIWebView::updateDocumentSceneContentExtent() {
expandDocumentContentExtent( body, mDocContainer->getPixelsPosition(), extent ); expandDocumentContentExtent( body, mDocContainer->getPixelsPosition(), extent );
} }
bool extentChanged = extent != Sizef::Zero && extent != mDocumentScene->getPixelsSize(); bool extentChanged = extent != Sizef::Zero && extent != mDocumentLayout->getPixelsSize();
if ( extentChanged ) { if ( extentChanged ) {
mDocumentLayout->setPixelsSize( extent );
mDocumentScene->setPixelsSize( extent ); mDocumentScene->setPixelsSize( extent );
updateHTMLMinHeightForDocument(); containerUpdate();
updateScroll();
} }
mDocumentExtentDirty = false;
mDocumentExtentDirtyReasons = 0;
mUpdatingDocumentContentExtent = false; mUpdatingDocumentContentExtent = false;
viewport = getDocumentViewportPixelsSize();
bool viewportChanged =
viewport != Sizef::Zero && viewport != mDocumentScene->getViewportPixelsSize();
if ( ( viewportChanged || extentChanged ) && !mDocumentViewportSyncQueued ) {
mDocumentViewportSyncQueued = true;
runOnMainThread( [this] {
runOnMainThread( [this] {
mDocumentViewportSyncQueued = false;
onDocumentViewportGeometryChanged();
} );
} );
}
}
void UIWebView::updateDocumentSceneMetrics() {
updateDocumentViewportMetrics();
updateDocumentSceneContentExtent();
} }
void UIWebView::setStyleSheetDefaultMarker( Uint32 marker ) { void UIWebView::setStyleSheetDefaultMarker( Uint32 marker ) {
@@ -615,8 +597,7 @@ void UIWebView::invalidateDocumentLayout( LayoutInvalidationFlags reasons ) {
reasons & ( toLayoutInvalidationFlags( LayoutInvalidationReason::DocumentExtent ) | reasons & ( toLayoutInvalidationFlags( LayoutInvalidationReason::DocumentExtent ) |
toLayoutInvalidationFlags( LayoutInvalidationReason::Viewport ) ); toLayoutInvalidationFlags( LayoutInvalidationReason::Viewport ) );
if ( docExtent ) { if ( docExtent ) {
containerUpdate(); markDocumentExtentDirty( reasons );
updateHTMLMinHeightForDocument();
} }
if ( mDocContainer && mDocContainer->isLayout() ) if ( mDocContainer && mDocContainer->isLayout() )
mDocContainer->asType<UILayout>()->setLayoutDirty( reasons ); mDocContainer->asType<UILayout>()->setLayoutDirty( reasons );
+215 -30
View File
@@ -6,6 +6,7 @@
#include <eepp/scene/scenemanager.hpp> #include <eepp/scene/scenemanager.hpp>
#include <eepp/system/filesystem.hpp> #include <eepp/system/filesystem.hpp>
#include <eepp/system/sys.hpp> #include <eepp/system/sys.hpp>
#include <eepp/ui/uilayout.hpp>
#include <eepp/ui/uiscenenode.hpp> #include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uiscrollbar.hpp> #include <eepp/ui/uiscrollbar.hpp>
#include <eepp/ui/uithememanager.hpp> #include <eepp/ui/uithememanager.hpp>
@@ -55,7 +56,8 @@ UTEST( UIWebView, OwnedDocumentSceneScrollTarget ) {
UISceneNode* documentScene = webView->getDocumentSceneNode(); UISceneNode* documentScene = webView->getDocumentSceneNode();
ASSERT_TRUE( documentScene != nullptr ); ASSERT_TRUE( documentScene != nullptr );
ASSERT_TRUE( documentScene->getParent() == webView->getContainer() ); ASSERT_TRUE( webView->getScrollView() == documentScene->getParent() );
ASSERT_TRUE( documentScene->getParent()->getParent() == webView->getContainer() );
EXPECT_EQ( SceneManager::instance()->count(), (size_t)1 ); EXPECT_EQ( SceneManager::instance()->count(), (size_t)1 );
Node* htmlNode = nullptr; Node* htmlNode = nullptr;
@@ -82,8 +84,10 @@ UTEST( UIWebView, OwnedDocumentSceneScrollTarget ) {
EXPECT_GT( documentScene->getPixelsSize().getHeight(), 1000.f ); EXPECT_GT( documentScene->getPixelsSize().getHeight(), 1000.f );
EXPECT_TRUE( webView->getVerticalScrollBar()->isVisible() ); EXPECT_TRUE( webView->getVerticalScrollBar()->isVisible() );
ASSERT_TRUE( documentScene->getParent() != nullptr && documentScene->getParent()->isUINode() );
UINode* scrollTarget = documentScene->getParent()->asType<UINode>();
webView->getVerticalScrollBar()->setValue( 1.f ); webView->getVerticalScrollBar()->setValue( 1.f );
EXPECT_LT( documentScene->getPosition().y, -500.f ); EXPECT_LT( scrollTarget->getPixelsPosition().y, -500.f );
Engine::destroySingleton(); Engine::destroySingleton();
} }
@@ -163,10 +167,12 @@ UTEST( UIWebView, VerticalScrollbarViewportDoesNotCreateHorizontalScroll ) {
EXPECT_NEAR( documentScene->getPixelsSize().getWidth(), stableExtent.getWidth(), 0.5f ); EXPECT_NEAR( documentScene->getPixelsSize().getWidth(), stableExtent.getWidth(), 0.5f );
EXPECT_NEAR( documentScene->getPixelsSize().getHeight(), stableExtent.getHeight(), 0.5f ); EXPECT_NEAR( documentScene->getPixelsSize().getHeight(), stableExtent.getHeight(), 0.5f );
ASSERT_TRUE( documentScene->getParent() != nullptr && documentScene->getParent()->isUINode() );
UINode* scrollTarget = documentScene->getParent()->asType<UINode>();
webView->getVerticalScrollBar()->setValue( 1.f ); webView->getVerticalScrollBar()->setValue( 1.f );
EXPECT_NEAR( -documentScene->getPosition().y + EXPECT_NEAR( -scrollTarget->getPixelsPosition().y +
webView->getContainer()->getPixelsSize().getHeight(), webView->getContainer()->getPixelsSize().getHeight(),
documentScene->getPixelsSize().getHeight(), 1.f ); scrollTarget->getPixelsSize().getHeight(), 1.f );
Engine::destroySingleton(); Engine::destroySingleton();
} }
@@ -278,9 +284,11 @@ UTEST( UIWebView, ExplicitWideDocumentHorizontalScrollReachesRightEdgeAtPixelDen
webView->getHorizontalScrollBar()->setValue( 1.f ); webView->getHorizontalScrollBar()->setValue( 1.f );
pump(); pump();
EXPECT_NEAR( -documentScene->getPosition().x + ASSERT_TRUE( documentScene->getParent() != nullptr && documentScene->getParent()->isUINode() );
UINode* scrollTarget = documentScene->getParent()->asType<UINode>();
EXPECT_NEAR( -scrollTarget->getPixelsPosition().x +
webView->getContainer()->getPixelsSize().getWidth(), webView->getContainer()->getPixelsSize().getWidth(),
documentScene->getPixelsSize().getWidth(), 1.f ); scrollTarget->getPixelsSize().getWidth(), 1.f );
Engine::destroySingleton(); Engine::destroySingleton();
EE::Graphics::PixelDensity::setPixelDensity( 1.0f ); EE::Graphics::PixelDensity::setPixelDensity( 1.0f );
@@ -431,7 +439,9 @@ UTEST( UIWebView, HorizontalScrollDisappearsAfterResponsiveShrink ) {
webView->getHorizontalScrollBar()->setValue( 1.f ); webView->getHorizontalScrollBar()->setValue( 1.f );
pump(); pump();
ASSERT_LT( documentScene->getPosition().x, -100.f ); ASSERT_TRUE( documentScene->getParent() != nullptr && documentScene->getParent()->isUINode() );
UINode* scrollTarget = documentScene->getParent()->asType<UINode>();
ASSERT_LT( scrollTarget->getPixelsPosition().x, -100.f );
webView->loadURI( URI( "file://" + fitPath ) ); webView->loadURI( URI( "file://" + fitPath ) );
pump(); pump();
@@ -443,17 +453,17 @@ UTEST( UIWebView, HorizontalScrollDisappearsAfterResponsiveShrink ) {
ASSERT_TRUE( html != nullptr ); ASSERT_TRUE( html != nullptr );
ASSERT_TRUE( body != nullptr ); ASSERT_TRUE( body != nullptr );
EXPECT_NEAR( documentScene->getRoot()->getPixelsSize().getWidth(), EXPECT_NEAR( documentScene->getRoot()->getPixelsSize().getWidth(),
documentScene->getPixelsSize().getWidth(), 0.5f ); documentScene->getViewportPixelsSize().getWidth(), 0.5f );
EXPECT_NEAR( fit->getPixelsSize().getWidth(), documentScene->getViewportPixelsSize().getWidth(), EXPECT_NEAR( fit->getPixelsSize().getWidth(), documentScene->getViewportPixelsSize().getWidth(),
0.5f ); 0.5f );
EXPECT_NEAR( html->getPixelsSize().getWidth(), documentScene->getPixelsSize().getWidth(), EXPECT_NEAR( html->getPixelsSize().getWidth(),
0.5f ); documentScene->getViewportPixelsSize().getWidth(), 0.5f );
EXPECT_NEAR( body->getPixelsSize().getWidth(), EXPECT_NEAR( body->getPixelsSize().getWidth(),
documentScene->getViewportPixelsSize().getWidth(), 0.5f ); documentScene->getViewportPixelsSize().getWidth(), 0.5f );
EXPECT_NEAR( documentScene->getPixelsSize().getWidth(), EXPECT_NEAR( documentScene->getPixelsSize().getWidth(),
documentScene->getViewportPixelsSize().getWidth(), 0.5f ); documentScene->getViewportPixelsSize().getWidth(), 0.5f );
EXPECT_FALSE( webView->getHorizontalScrollBar()->isVisible() ); EXPECT_FALSE( webView->getHorizontalScrollBar()->isVisible() );
EXPECT_NEAR( documentScene->getPosition().x, 0.f, 0.5f ); EXPECT_NEAR( scrollTarget->getPixelsPosition().x, 0.f, 0.5f );
Engine::destroySingleton(); Engine::destroySingleton();
} }
@@ -708,6 +718,8 @@ UTEST( UIWebView, HackerNewsFrontPageBottomIsReachable ) {
ASSERT_TRUE( html != nullptr ); ASSERT_TRUE( html != nullptr );
ASSERT_TRUE( body != nullptr ); ASSERT_TRUE( body != nullptr );
ASSERT_TRUE( webView->getVerticalScrollBar()->isVisible() ); ASSERT_TRUE( webView->getVerticalScrollBar()->isVisible() );
ASSERT_TRUE( documentScene->getParent() != nullptr && documentScene->getParent()->isUINode() );
UINode* scrollTarget = documentScene->getParent()->asType<UINode>();
Float viewportHeight = webView->getContainer()->getPixelsSize().getHeight(); Float viewportHeight = webView->getContainer()->getPixelsSize().getHeight();
const Float inputBottomInDocument = const Float inputBottomInDocument =
@@ -717,9 +729,9 @@ UTEST( UIWebView, HackerNewsFrontPageBottomIsReachable ) {
EXPECT_GE( documentScene->getPixelsSize().getWidth(), EXPECT_GE( documentScene->getPixelsSize().getWidth(),
documentScene->getViewportPixelsSize().getWidth() ); documentScene->getViewportPixelsSize().getWidth() );
EXPECT_NEAR( documentScene->getRoot()->getPixelsSize().getWidth(), EXPECT_NEAR( documentScene->getRoot()->getPixelsSize().getWidth(),
documentScene->getPixelsSize().getWidth(), 0.5f ); documentScene->getViewportPixelsSize().getWidth(), 0.5f );
EXPECT_NEAR( html->getPixelsSize().getWidth(), documentScene->getPixelsSize().getWidth(), EXPECT_NEAR( html->getPixelsSize().getWidth(),
0.5f ); documentScene->getViewportPixelsSize().getWidth(), 0.5f );
EXPECT_NEAR( documentScene->getPixelsSize().getHeight(), EXPECT_NEAR( documentScene->getPixelsSize().getHeight(),
html->fitMinMaxSizePx( html->getPixelsSize() ).getHeight(), 0.5f ); html->fitMinMaxSizePx( html->getPixelsSize() ).getHeight(), 0.5f );
@@ -739,9 +751,9 @@ UTEST( UIWebView, HackerNewsFrontPageBottomIsReachable ) {
EXPECT_GE( documentScene->getPixelsSize().getWidth(), EXPECT_GE( documentScene->getPixelsSize().getWidth(),
documentScene->getViewportPixelsSize().getWidth() ); documentScene->getViewportPixelsSize().getWidth() );
EXPECT_NEAR( documentScene->getRoot()->getPixelsSize().getWidth(), EXPECT_NEAR( documentScene->getRoot()->getPixelsSize().getWidth(),
documentScene->getPixelsSize().getWidth(), 0.5f ); documentScene->getViewportPixelsSize().getWidth(), 0.5f );
EXPECT_NEAR( html->getPixelsSize().getWidth(), documentScene->getPixelsSize().getWidth(), EXPECT_NEAR( html->getPixelsSize().getWidth(),
0.5f ); documentScene->getViewportPixelsSize().getWidth(), 0.5f );
EXPECT_NEAR( documentScene->getPixelsSize().getHeight(), EXPECT_NEAR( documentScene->getPixelsSize().getHeight(),
html->fitMinMaxSizePx( html->getPixelsSize() ).getHeight(), 0.5f ); html->fitMinMaxSizePx( html->getPixelsSize() ).getHeight(), 0.5f );
viewportHeight = webView->getContainer()->getPixelsSize().getHeight(); viewportHeight = webView->getContainer()->getPixelsSize().getHeight();
@@ -757,9 +769,9 @@ UTEST( UIWebView, HackerNewsFrontPageBottomIsReachable ) {
EXPECT_GE( documentScene->getPixelsSize().getWidth(), EXPECT_GE( documentScene->getPixelsSize().getWidth(),
documentScene->getViewportPixelsSize().getWidth() ); documentScene->getViewportPixelsSize().getWidth() );
EXPECT_NEAR( documentScene->getRoot()->getPixelsSize().getWidth(), EXPECT_NEAR( documentScene->getRoot()->getPixelsSize().getWidth(),
documentScene->getPixelsSize().getWidth(), 0.5f ); documentScene->getViewportPixelsSize().getWidth(), 0.5f );
EXPECT_NEAR( html->getPixelsSize().getWidth(), documentScene->getPixelsSize().getWidth(), EXPECT_NEAR( html->getPixelsSize().getWidth(),
0.5f ); documentScene->getViewportPixelsSize().getWidth(), 0.5f );
EXPECT_NEAR( documentScene->getPixelsSize().getHeight(), EXPECT_NEAR( documentScene->getPixelsSize().getHeight(),
html->fitMinMaxSizePx( html->getPixelsSize() ).getHeight(), 0.5f ); html->fitMinMaxSizePx( html->getPixelsSize() ).getHeight(), 0.5f );
viewportHeight = webView->getContainer()->getPixelsSize().getHeight(); viewportHeight = webView->getContainer()->getPixelsSize().getHeight();
@@ -771,10 +783,10 @@ UTEST( UIWebView, HackerNewsFrontPageBottomIsReachable ) {
const Rectf containerRect = webView->getContainer()->getScreenRect(); const Rectf containerRect = webView->getContainer()->getScreenRect();
EXPECT_LE( inputRect.Bottom, containerRect.Bottom + 1.f ); EXPECT_LE( inputRect.Bottom, containerRect.Bottom + 1.f );
EXPECT_GT( inputRect.Bottom, containerRect.Top ); EXPECT_GT( inputRect.Bottom, containerRect.Top );
EXPECT_NEAR( -documentScene->getPosition().y + viewportHeight, EXPECT_NEAR( -scrollTarget->getPixelsPosition().y + viewportHeight,
documentScene->getPixelsSize().getHeight(), 1.f ); scrollTarget->getPixelsSize().getHeight(), 1.f );
EXPECT_NEAR( documentScene->getRoot()->getPixelsSize().getHeight(), EXPECT_NEAR( documentScene->getRoot()->getPixelsSize().getHeight(),
documentScene->getPixelsSize().getHeight(), 0.5f ); documentScene->getViewportPixelsSize().getHeight(), 0.5f );
const Vector2f inputCenter( inputRect.Left + inputRect.getWidth() * 0.5f, const Vector2f inputCenter( inputRect.Left + inputRect.getWidth() * 0.5f,
inputRect.Top + inputRect.getHeight() * 0.5f ); inputRect.Top + inputRect.getHeight() * 0.5f );
@@ -846,7 +858,7 @@ UTEST( UIWebView, NavigationAfterGrowDoesNotKeepMaximizedWidthOnShrink ) {
webView->getContainer()->getPixelsSize().getWidth(), 0.5f ); webView->getContainer()->getPixelsSize().getWidth(), 0.5f );
EXPECT_LT( documentScene->getViewportPixelsSize().getWidth(), 900.f ); EXPECT_LT( documentScene->getViewportPixelsSize().getWidth(), 900.f );
EXPECT_NEAR( documentScene->getRoot()->getPixelsSize().getWidth(), EXPECT_NEAR( documentScene->getRoot()->getPixelsSize().getWidth(),
documentScene->getPixelsSize().getWidth(), 0.5f ); documentScene->getViewportPixelsSize().getWidth(), 0.5f );
EXPECT_LT( html->fitMinMaxSizePx( html->getPixelsSize() ).getWidth(), 900.f ); EXPECT_LT( html->fitMinMaxSizePx( html->getPixelsSize() ).getWidth(), 900.f );
EXPECT_LT( body->fitMinMaxSizePx( body->getPixelsSize() ).getWidth(), 900.f ); EXPECT_LT( body->fitMinMaxSizePx( body->getPixelsSize() ).getWidth(), 900.f );
EXPECT_LT( documentScene->getPixelsSize().getWidth(), 900.f ); EXPECT_LT( documentScene->getPixelsSize().getWidth(), 900.f );
@@ -855,6 +867,177 @@ UTEST( UIWebView, NavigationAfterGrowDoesNotKeepMaximizedWidthOnShrink ) {
Engine::destroySingleton(); Engine::destroySingleton();
} }
UTEST( UIWebView, NavigationFromTallToShortShrinksDocumentExtent ) {
auto win = Engine::instance()->createWindow(
WindowSettings( 640, 480, "UIWebView Navigation Extent Shrink Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false, true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
ASSERT_TRUE( font != nullptr && font->loaded() );
FontFamily::loadFromRegular( font );
UISceneNode* sceneNode = UISceneNode::New();
SceneManager::instance()->add( sceneNode );
sceneNode->getUIThemeManager()->setDefaultFont( font );
UIWebView* webView = UIWebView::New();
webView->setParent( sceneNode->getRoot() );
webView->setPixelsSize( 400, 300 );
webView->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
const std::string tallPath = Sys::getTempPath() + "eepp_uiwebview_tall.html";
FileSystem::fileWrite( tallPath, R"html(
<!DOCTYPE html>
<html>
<head><style>html, body { margin: 0; padding: 0; }</style></head>
<body><div id="content" style="height: 1800px; width: 100%;"></div></body>
</html>
)html" );
const std::string shortPath = Sys::getTempPath() + "eepp_uiwebview_short.html";
FileSystem::fileWrite( shortPath, R"html(
<!DOCTYPE html>
<html>
<head><style>html, body { margin: 0; padding: 0; }</style></head>
<body><div id="content" style="height: 120px; width: 100%;"></div></body>
</html>
)html" );
auto pump = [&]() {
for ( int i = 0; i < 20; i++ ) {
win->getInput()->update();
SceneManager::instance()->update( Seconds( 1.f / 60.f ) );
}
};
webView->loadURI( URI( "file://" + tallPath ) );
UISceneNode* documentScene = webView->getDocumentSceneNode();
ASSERT_TRUE( documentScene != nullptr );
pump();
ASSERT_TRUE( documentScene->getParent() != nullptr && documentScene->getParent()->isUINode() );
UINode* scrollTarget = documentScene->getParent()->asType<UINode>();
EXPECT_GT( scrollTarget->getPixelsSize().getHeight(), 1700.f );
webView->loadURI( URI( "file://" + shortPath ) );
pump();
EXPECT_NEAR( scrollTarget->getPixelsSize().getHeight(),
webView->getContainer()->getPixelsSize().getHeight(), 1.f );
EXPECT_NEAR( documentScene->getPixelsSize().getHeight(),
webView->getContainer()->getPixelsSize().getHeight(), 1.f );
EXPECT_FALSE( webView->getVerticalScrollBar()->isVisible() );
Engine::destroySingleton();
}
UTEST( UIWebView, AsyncCSSCanShrinkDocumentExtent ) {
auto win = Engine::instance()->createWindow(
WindowSettings( 640, 480, "UIWebView CSS Extent Shrink Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false, true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
ASSERT_TRUE( font != nullptr && font->loaded() );
FontFamily::loadFromRegular( font );
UISceneNode* sceneNode = UISceneNode::New();
SceneManager::instance()->add( sceneNode );
sceneNode->getUIThemeManager()->setDefaultFont( font );
UIWebView* webView = UIWebView::New();
webView->setParent( sceneNode->getRoot() );
webView->setPixelsSize( 400, 300 );
webView->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
const std::string path = Sys::getTempPath() + "eepp_uiwebview_css_shrink.html";
FileSystem::fileWrite( path, R"html(
<!DOCTYPE html>
<html>
<head><style>html, body { margin: 0; padding: 0; } #content { height: 1800px; }</style></head>
<body><div id="content"></div></body>
</html>
)html" );
auto pump = [&]() {
for ( int i = 0; i < 20; i++ ) {
win->getInput()->update();
SceneManager::instance()->update( Seconds( 1.f / 60.f ) );
}
};
webView->loadURI( URI( "file://" + path ) );
UISceneNode* documentScene = webView->getDocumentSceneNode();
ASSERT_TRUE( documentScene != nullptr );
pump();
ASSERT_TRUE( documentScene->getParent() != nullptr && documentScene->getParent()->isUINode() );
UINode* scrollTarget = documentScene->getParent()->asType<UINode>();
EXPECT_GT( scrollTarget->getPixelsSize().getHeight(), 1700.f );
documentScene->combineStyleSheet( "#content { height: 120px; }", true );
pump();
EXPECT_NEAR( scrollTarget->getPixelsSize().getHeight(),
webView->getContainer()->getPixelsSize().getHeight(), 1.f );
EXPECT_NEAR( documentScene->getPixelsSize().getHeight(),
webView->getContainer()->getPixelsSize().getHeight(), 1.f );
EXPECT_FALSE( webView->getVerticalScrollBar()->isVisible() );
Engine::destroySingleton();
}
UTEST( UIWebView, CoalescesViewportResizeDocumentMetrics ) {
auto win = Engine::instance()->createWindow(
WindowSettings( 800, 600, "UIWebView Resize Metrics Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false, true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
ASSERT_TRUE( font != nullptr && font->loaded() );
FontFamily::loadFromRegular( font );
UISceneNode* sceneNode = UISceneNode::New();
SceneManager::instance()->add( sceneNode );
sceneNode->getUIThemeManager()->setDefaultFont( font );
UIWebView* webView = UIWebView::New();
webView->setParent( sceneNode->getRoot() );
webView->setPixelsSize( 800, 500 );
webView->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
webView->loadURI( URI( "file://" + Sys::getProcessPath() + "assets/html/hn_frontpage.html" ) );
auto pump = [&]() {
for ( int i = 0; i < 30; i++ ) {
win->getInput()->update();
SceneManager::instance()->update( Seconds( 1.f / 60.f ) );
}
};
pump();
ASSERT_GE( webView->getDocumentSceneNode()->getRoot()->querySelectorAll( ".athing" ).size(),
(size_t)30 );
UILayout::resetMetrics();
webView->setPixelsSize( 900, 530 );
webView->setPixelsSize( 700, 480 );
webView->setPixelsSize( 1000, 600 );
webView->setPixelsSize( 800, 500 );
for ( int frame = 0; frame < 6; frame++ ) {
win->getInput()->update();
SceneManager::instance()->update( Seconds( 1.f / 60.f ) );
}
auto metrics = UILayout::getMetrics();
UILayout::setMetricsEnabled( false );
EXPECT_LE( metrics.treeUpdates, (Uint64)4 );
EXPECT_EQ( metrics.synchronousUpdates, (Uint64)0 );
EXPECT_EQ( metrics.richTextRebuilds, (Uint64)0 );
Engine::destroySingleton();
}
UTEST( UIWebView, HackerNewsSingleStepRestoreSettlesViewportInOneFrame ) { UTEST( UIWebView, HackerNewsSingleStepRestoreSettlesViewportInOneFrame ) {
auto win = Engine::instance()->createWindow( auto win = Engine::instance()->createWindow(
WindowSettings( 1024, 650, "UIWebView Single Step Restore Test", WindowStyle::Default, WindowSettings( 1024, 650, "UIWebView Single Step Restore Test", WindowStyle::Default,
@@ -982,7 +1165,7 @@ UTEST( UIWebView, LayoutDrivenResizeKeepsDocumentRootAtViewport ) {
pump(); pump();
EXPECT_GT( documentScene->getViewportPixelsSize().getWidth(), 2000.f ); EXPECT_GT( documentScene->getViewportPixelsSize().getWidth(), 2000.f );
EXPECT_NEAR( documentScene->getRoot()->getPixelsSize().getWidth(), EXPECT_NEAR( documentScene->getRoot()->getPixelsSize().getWidth(),
documentScene->getPixelsSize().getWidth(), 0.5f ); documentScene->getViewportPixelsSize().getWidth(), 0.5f );
webView->loadURI( URI( "file://" + Sys::getProcessPath() + "assets/html/hn_frontpage.html" ) ); webView->loadURI( URI( "file://" + Sys::getProcessPath() + "assets/html/hn_frontpage.html" ) );
pump(); pump();
@@ -997,7 +1180,7 @@ UTEST( UIWebView, LayoutDrivenResizeKeepsDocumentRootAtViewport ) {
ASSERT_TRUE( body != nullptr ); ASSERT_TRUE( body != nullptr );
EXPECT_LT( documentScene->getViewportPixelsSize().getWidth(), 900.f ); EXPECT_LT( documentScene->getViewportPixelsSize().getWidth(), 900.f );
EXPECT_NEAR( documentScene->getRoot()->getPixelsSize().getWidth(), EXPECT_NEAR( documentScene->getRoot()->getPixelsSize().getWidth(),
documentScene->getPixelsSize().getWidth(), 0.5f ); documentScene->getViewportPixelsSize().getWidth(), 0.5f );
EXPECT_NEAR( html->getPixelsSize().getWidth(), EXPECT_NEAR( html->getPixelsSize().getWidth(),
documentScene->getViewportPixelsSize().getWidth(), 0.5f ); documentScene->getViewportPixelsSize().getWidth(), 0.5f );
EXPECT_LE( body->getPixelsSize().getWidth(), EXPECT_LE( body->getPixelsSize().getWidth(),
@@ -1015,13 +1198,15 @@ UTEST( UIWebView, LayoutDrivenResizeKeepsDocumentRootAtViewport ) {
webView->getVerticalScrollBar()->setValue( 1.f ); webView->getVerticalScrollBar()->setValue( 1.f );
webView->getHorizontalScrollBar()->setValue( 1.f ); webView->getHorizontalScrollBar()->setValue( 1.f );
pump(); pump();
EXPECT_NEAR( -documentScene->getPosition().y + ASSERT_TRUE( documentScene->getParent() != nullptr && documentScene->getParent()->isUINode() );
UINode* scrollTarget = documentScene->getParent()->asType<UINode>();
EXPECT_NEAR( -scrollTarget->getPixelsPosition().y +
webView->getContainer()->getPixelsSize().getHeight(), webView->getContainer()->getPixelsSize().getHeight(),
documentScene->getPixelsSize().getHeight(), 1.f ); scrollTarget->getPixelsSize().getHeight(), 1.f );
if ( webView->getHorizontalScrollBar()->isVisible() ) { if ( webView->getHorizontalScrollBar()->isVisible() ) {
EXPECT_NEAR( -documentScene->getPosition().x + EXPECT_NEAR( -scrollTarget->getPixelsPosition().x +
webView->getContainer()->getPixelsSize().getWidth(), webView->getContainer()->getPixelsSize().getWidth(),
documentScene->getPixelsSize().getWidth(), 1.f ); scrollTarget->getPixelsSize().getWidth(), 1.f );
} }
Engine::destroySingleton(); Engine::destroySingleton();