mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-02 11:10:31 +03:00
Greatly improved UIWebView implementation.
This commit is contained in:
@@ -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.
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 );
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user