diff --git a/.agent/plans/uiwebview_document_scene_plan.md b/.agent/plans/uiwebview_document_scene_plan.md new file mode 100644 index 000000000..9637afdff --- /dev/null +++ b/.agent/plans/uiwebview_document_scene_plan.md @@ -0,0 +1,512 @@ +# UIWebView Document Scene Isolation Plan + +> Status: PROPOSED - investigated against the current `UIWebView`, `UISceneNode`, +> `UIScrollView`, HTML loading, and nested-scene behavior. + +## Goal + +Make every `UIWebView` own an independent `UISceneNode` that represents its loaded HTML document. +All widgets created from the document, all document styles, URI resolution, navigation interception, +media-query evaluation, dirty style/layout queues, and document resources must use that owned scene +instead of the application `UISceneNode`. + +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 +web views and is the foundation for future `iframe` support. + +## Required Invariants + +After implementation: + +- The application scene stylesheet never matches elements inside a `UIWebView`. +- A document stylesheet never matches application widgets or another `UIWebView`. +- Loading or navigating one web view never removes or changes another scene's styles, URI, + referer, navigation callback, dirty queues, or document lookup results. +- Every loaded HTML widget returns the web view's owned scene from `getUISceneNode()` and + `getSceneNode()`. +- The owned scene is attached to the application tree but is not registered as a top-level + `SceneManager` scene. +- The owned scene size follows the scrollable HTML/body document extent. +- The document scene separately stores the visible web-view viewport size for viewport units, media + queries, document minimum height, and fixed-position semantics. +- 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 + callbacks, actions, listeners, or dirty pointers behind. + +## Current State + +### UIWebView Uses The Application Scene As Its Document + +`UIWebView` currently creates `mDocContainer` directly under the web view. `UIScrollView` reparents +that container into its clipped internal container and uses it as the scroll target. + +`UIWebView::loadDocumentData()` then uses `getUISceneNode()`, which is the application scene, to: + +- remove all stylesheet rules except `mStyleSheetDefaultMarker`, +- set the document URI and referer, +- load the HTML tree into `mDocContainer`, +- install a navigation interceptor. + +`onSizeChange()` and `updateHTMLMinHeightForDocument()` also search the entire application scene for +the first HTML and body nodes. With more than one document, those lookups are ambiguous. + +This means one navigation can erase application or sibling-document CSS, relative URLs use whichever +document URI was set last, and the last web view to load replaces the global navigation interceptor. + +### Nested UISceneNode Support Already Exists + +`UISceneNode` is a `SceneNode` and can be nested in a UI tree. The UI editor demonstrates this by +attaching its preview `UISceneNode` below an application widget. + +When a `UISceneNode` receives a parent: + +- its own root and descendants remain associated with that nested scene, +- it retains its own stylesheet, URI, resource state, and dirty queues, +- it adopts the ancestor UI scene's event dispatcher, +- it follows its direct parent's pixel size. + +This is the correct base mechanism, but the UI editor manually registers and updates both scenes. +An owned web-view scene must not require top-level `SceneManager` registration. + +### Viewport And Scroll Extent Are Currently Conflated + +The owned scene must be the `UIScrollView` scroll target and must grow to the HTML/body document +extent. 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. + +`UISceneNode` currently also uses its own size for viewport-dependent CSS behavior. Once the scene is +content-sized, those uses must not read the scene extent: + +- `UISceneNode::getMediaFeatures()` must report the visible web-view viewport. +- `vw`/`vh` and related viewport-relative lengths must resolve against 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 + is translated by scrolling. + +The design therefore needs separate scene extent and viewport metrics. The scene extent participates +in scrolling; the viewport size is provided by the embedding `UIWebView`. + +## Proposed Scene Tree + +```text +application UISceneNode +└── ... application widgets ... + └── UIWebView + └── UIScrollView::mContainer host-scene clipping shell + └── UIWebView::mDocumentScene owned UISceneNode, content-sized scroll target + └── UISceneNode::mRoot owned scene root, content-sized + └── UIWebView::mDocContainer document layout container + └── html + └── body + └── document content +``` + +`mDocumentScene` is added as the `UIWebView` child through the normal `UIScrollView` path. +`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. + +The visible `mContainer` size is separately passed to `mDocumentScene` as its CSS viewport size. +Every document node, including `mDocContainer`, belongs to the owned scene. + +## Ownership And Service Policy + +The document boundary should distinguish document-owned state from embedding-platform services. + +### Owned Per UIWebView + +- `UISceneNode` root and document widgets +- stylesheet, keyframes, media-list state, and document markers +- dirty style, style-state, and layout queues +- URI, referer, and relative-resource resolution +- navigation interceptor +- cookie manager for the initial implementation +- document-scoped `@font-face` aliases and loaded-font tracking +- document actions and scheduled updates + +The host stylesheet must never be copied or combined into the document scene. + +### Explicitly Inherited Or Shared From The Host Scene + +- window and DPI +- shared `UIEventDispatcher` +- thread pool +- color-scheme and contrast preferences +- default font, default font size, and default theme pointer needed by native form controls + +Add one narrow `UISceneNode` helper for initializing an embedded scene from its host scene. The +helper must copy only the service/configuration values above; it must not copy stylesheet, URI, +referer, navigation callback, cookies, dirty queues, actions, or roots. + +The owned scene's theme manager remains a separate manager. Sharing non-owning font/theme pointers is +acceptable because the document scene is a descendant and is destroyed before the host scene. +Do not copy icon-theme ownership into the document scene because `UIIconThemeManager` owns themes. + +Future `iframe` work should replace the per-scene cookie policy with an explicit shared browsing +session/context. That should not block document-scene isolation. + +`FontManager` remains an engine-wide resource manager for application and system fonts. Author +`@font-face` family names must be resolved through a document-scoped alias registry before falling +back to that global manager; otherwise two documents declaring the same family name can still +interfere. + +## Public API + +Add: + +```cpp +UISceneNode* UIWebView::getDocumentSceneNode() const; +``` + +Keep: + +```cpp +UIWidget* UIWebView::getDocumentContainer() const; +``` + +`getDocumentContainer()` continues returning the content container, but it will now belong to the +document scene. Applications that intentionally inject document CSS must use +`getDocumentSceneNode()->combineStyleSheet(...)`, not the application scene. + +Keep `setStyleSheetDefaultMarker()` for compatibility, but document that it controls which rules +persist across navigations inside this web view. The default remains the HTML defaults marker. + +## Implementation Plan + +### Phase 1: Separate UISceneNode Extent From Viewport Metrics + +**Files:** + +- `include/eepp/ui/uiscenenode.hpp` +- `src/eepp/ui/uiscenenode.cpp` +- `include/eepp/ui/uinode.hpp` +- `src/eepp/ui/uinode.cpp` +- `src/eepp/ui/uirichtext.cpp` +- focused scene/viewport tests + +Steps: + +1. Add explicit viewport metrics to `UISceneNode`: + + ```cpp + void setViewportPixelsSize( const Sizef& size ); + void clearViewportPixelsSize(); + const Sizef& getViewportPixelsSize() const; + ``` + + When no override is set, `getViewportPixelsSize()` returns the scene's own pixel size so existing + 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 + current default for existing nested scenes; `UIWebView` disables it because its document scene + extent is content-driven. +3. Make `UISceneNode::getMediaFeatures()` use `getViewportPixelsSize()` for viewport width/height + while keeping device metrics from the window. +4. Audit viewport-relative length conversion. Replace direct `getSceneNode()->getPixelsSize()` use + 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. Keep ordinary scene/root sizing and world bounds based on the actual content-sized scene extent. + +Tests: + +- 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 + 600px. +- HTML/body minimum height uses the viewport while document/root extent can grow beyond it. +- Changing viewport width re-evaluates media queries without forcing the scene extent to viewport + height. + +### Phase 2: Harden Embedded UISceneNode Behavior + +**Files:** + +- `include/eepp/ui/uiscenenode.hpp` +- `src/eepp/ui/uiscenenode.cpp` +- focused nested-scene tests + +Steps: + +1. Add a helper that initializes an embedded scene from a host `UISceneNode` according to the + service policy above. +2. Override `UISceneNode::onSceneChange()` so moving an embedding subtree to another application + scene rebinds the nested scene's shared event dispatcher and inherited host services. Current + `onParentChange()` alone does not run when only an ancestor changes scene. +3. Preserve the current rule that a nested scene does not own/delete the shared event dispatcher. +4. Add comments documenting that nested scenes need an owner-driven update unless they are + top-level `SceneManager` scenes. + +Tests: + +- Widgets below a nested scene resolve `getUISceneNode()` to the nested scene. +- Host and nested stylesheets select only their own descendants. +- Nested media queries use the explicit embedded viewport size, not the window or content extent. +- Reparenting the embedding subtree to another host scene rebinds the dispatcher/services. +- Deleting a nested scene does not delete or corrupt the host dispatcher. + +### Phase 3: Give UIWebView An Owned Document Scene + +**Files:** + +- `include/eepp/ui/uiwebview.hpp` +- `src/eepp/ui/uiwebview.cpp` +- `src/tests/unit_tests/uiwebview_tests.cpp` + +Steps: + +1. Add `UISceneNode* mDocumentScene`. +2. Construct the owned scene with the host window, initialize its embedded services, and add it as + the `UIWebView` scroll child. Let the normal `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, + match-width, wrap-content behavior and white initial background. +4. Disable automatic parent-size following for `mDocumentScene`. +5. Feed the visible `UIScrollView::mContainer` size into + `mDocumentScene->setViewportPixelsSize()`. +6. Size `mDocumentScene` to at least the viewport width/height and grow it to the laid-out + HTML/body/document extent. Its size is the size observed by `UIScrollView` for scrollbar + calculations. +7. Listen for document extent and visible-container changes so scene extent, viewport metrics, + media queries, and scrollbar state are updated in the correct order without layout loops. +8. Override `scheduledUpdate()` in `UIWebView`, call the inherited + `UITouchDraggableWidget::scheduledUpdate()` behavior, and then call + `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 + scene to `SceneManager`. +9. Expose `getDocumentSceneNode()`. +10. On web-view scene changes, reinitialize/rebind the document scene's inherited services without + touching document-owned state. + +Tests: + +- The document scene is attached below the host scene but absent from `SceneManager`. +- `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. +- Scrolling translates the content-sized document scene inside the fixed `UIScrollView` viewport. +- `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. +- Document actions/animations and dirty style/layout queues update through the web view. + +### Phase 4: Route All Document Operations To The Owned Scene + +**Files:** + +- `src/eepp/ui/uiwebview.cpp` +- `src/examples/ui_html/ui_html.cpp` +- relevant tests + +Steps: + +1. Replace every document-related use of `UIWebView::getUISceneNode()` with `mDocumentScene`: + stylesheet cleanup, URI/referer, cookie manager, layout loading, relative navigation resolution, + navigation interception, and HTML/body lookup. +2. Restrict HTML/body lookup to `mDocumentScene->getRoot()` or `mDocContainer`; never search the host + scene. +3. Keep navigation events emitted by `UIWebView` itself so application listeners remain on the host + widget. +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. +5. On navigation, close only `mDocContainer` children and remove only nonpersistent rules from the + document scene. +6. Update the HTML example's injected Hacker News stylesheet to combine into + `webView->getDocumentSceneNode()`. +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 + correct. + +Tests: + +- Two web views can load documents with conflicting `body`, class, id, keyframe, and CSS-variable + names without cross-application. +- Host application CSS does not affect document nodes, and document CSS does not affect host + widgets. +- Navigating one web view does not change the other's stylesheet, URI, referer, cookies, navigation + callback, or DOM. +- Relative image, link, form, inline-style, external CSS, and `@font-face` URLs resolve from the + correct document URI. +- Application-level `findByType(UI_TYPE_HTML_HTML)` no longer returns a web-view document node; + document-scene lookup does. + +### Phase 5: Isolate Author Font Faces + +**Files:** + +- `include/eepp/ui/uiscenenode.hpp` +- `src/eepp/ui/uiscenenode.cpp` +- `src/tests/unit_tests/uiwebview_tests.cpp` + +`UISceneNode::loadFontFaces()` currently tracks loaded fonts per scene, but creates them with the CSS +family name and `getFontFromNamesList()` resolves through the global `FontManager`. Scene ownership +alone therefore does not isolate two documents that declare the same `@font-face` family. + +Steps: + +1. Add a document/scene-local font-face alias registry keyed by CSS family, style, and weight. +2. Make `getFontFromNamesList()` consult the local registry before global application/system fonts. +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. +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. +6. Apply the async scene-lifetime guard from the next phase to remote `@font-face` loads. + +Tests: + +- Two web views can declare the same family name with different font files and each resolves its own + font. +- Destroying or navigating one web view does not remove or replace the sibling's author font. +- Application/system font lookup still works when no document-local face matches. + +### Phase 6: Make Navigation And Resource Loading Lifetime-Safe + +**Files:** + +- `include/eepp/ui/uiwebview.hpp` +- `src/eepp/ui/uiwebview.cpp` +- `include/eepp/ui/uiscenenode.hpp` +- `src/eepp/ui/uiscenenode.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 +correct cancellation and stale-response handling part of the document lifecycle. The same audit +must cover async document resources such as `UISceneNode::loadCSS()`, which currently captures the +scene directly. + +Steps: + +1. Add a small shared navigation/load state containing an alive owner pointer and monotonically + increasing navigation generation. +2. Async callbacks capture a weak load state, not raw `this` or raw scene pointers. +3. Before storing cookies, posting to the main thread, or applying a response, verify that the web + view still exists and the generation is current. +4. A newer navigation invalidates older responses. Destruction invalidates all pending callbacks. +5. Keep history mutation and navigation events deterministic when requests fail or are superseded. +6. 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 + pattern where they retain scene/widget pointers. + +Tests: + +- Destroying a web view before an HTTP response completes is safe. +- A slow old response cannot replace a newer document. +- 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. + +### Phase 7: Integration And Documentation + +**Files:** + +- `.agent/rules/html-layout-architecture.md` +- `src/tests/unit_tests/uihtml_tests.cpp` for realistic fixture coverage where useful +- `src/examples/ui_html/ui_html.cpp` + +Steps: + +1. Document the web-document scene boundary, viewport/content split, and host-service inheritance. +2. Add a realistic two-web-view integration test with visibly conflicting CSS and independent + relative resources. +3. Run the existing old Reddit `UIWebView` smoke test against the new document scene. +4. Verify the UI editor nested scene still behaves correctly after nested-scene hardening. +5. Verify inspector/debug tooling can target either the application scene or + `getDocumentSceneNode()` explicitly. + +## Files Expected To Change + +| File | Change Summary | +|---|---| +| `include/eepp/ui/uiscenenode.hpp` | Separate viewport metrics, embedded sizing policy, host-service binding | +| `src/eepp/ui/uiscenenode.cpp` | Viewport media size, nested reparent hardening, document resources | +| `include/eepp/ui/uinode.hpp` | Viewport-relative CSS conversion support if a helper is required | +| `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 | +| `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/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/uihtml_tests.cpp` | Existing realistic web-view fixture adjustments | +| `.agent/rules/html-layout-architecture.md` | Document final architecture after implementation | + +No new production source file is required for the initial implementation. If the embedded-scene +service policy grows beyond the narrow helper above, introduce a dedicated browsing/document context +only when implementing `iframe` or shared browser-session behavior. + +## Rejected Approaches + +### Keep One UISceneNode And Add Stylesheet Markers + +Markers help remove rules but do not isolate selector matching, URI/referer, navigation interception, +media state, dirty queues, cookies, font faces, or DOM lookup. This does not create a document. + +### 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 +and media queries. Keep one content-sized scene and add explicit viewport metrics instead of adding +an extra nested scroll target or treating the content extent as the viewport. + +### Add Every Document Scene To SceneManager + +This copies the UI editor's manual arrangement but gives a web-view-owned scene top-level lifecycle. +It risks duplicate update/draw and prevents clean ownership for nested web views and future iframes. + +### Copy The Host Stylesheet Into The Document Scene + +This recreates the original leakage with a snapshot and makes later host-style updates inconsistent. +Only explicit document/user styles belong in the document scene. + +## Validation + +During implementation, validate each phase before proceeding: + +```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 +``` + +`projects/scripts/xvfb-run-eepp` is the required Linux/FreeBSD test wrapper. It enables automatic +server-number selection, so independent focused test runs may execute concurrently when useful. + +Run the full unit suite with: + +```sh +projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug +``` + +Run it after the focused isolation, scrolling, positioned-layout, media-query, and nested-scene +tests pass. + +## Phase Backup Stashes + +Each implementation phase must end with a persistent Git stash backup after that phase's focused +tests pass. + +The phase stash is a cumulative snapshot of the complete working tree at that passing phase, +including new untracked source and test files. Immediately restore the snapshot into the working tree +with `git stash apply`, leaving the backup entry permanently present in the stash list: + +```sh +git status --short +git stash push --include-untracked -m "uiwebview-document-scene: phase passing" +git stash apply --index stash@{0} +git status --short +git stash list +``` + +Rules: + +- Create the phase stash only after the phase's required focused tests pass. +- Include untracked files so newly added production files, tests, and fixtures are backed up. +- Use `git stash apply --index`, never `git stash pop`, to restore the phase snapshot while + preserving the stash. +- Never run `git stash drop`, `git stash clear`, or any other operation that removes these phase + backup stashes. +- Never overwrite or replace a previous phase stash. Every passing phase keeps its own named, + cumulative backup. +- After restoring a phase stash, verify both that the expected changes remain in the working tree + and that the named phase stash remains in `git stash list`. +- If a phase stash is restored later, use `git stash apply --index` and keep the source stash entry. +- If applying a stash conflicts, resolve the working tree without dropping or popping the stash. + +## Completion Criteria + +The design is complete when two `UIWebView` instances can coexist in one application scene with +conflicting documents and independently navigate, style, resolve resources, update, receive input, +and scroll without observable state leakage into each other or the application UI. diff --git a/.agent/rules/html-layout-architecture.md b/.agent/rules/html-layout-architecture.md index 2f18c5f97..0cb9d0cf9 100644 --- a/.agent/rules/html-layout-architecture.md +++ b/.agent/rules/html-layout-architecture.md @@ -28,6 +28,54 @@ Required workflow for new HTML/CSS behavior: ## Core Concepts +### UIWebView Document Scenes + +Each `UIWebView` owns a nested `UISceneNode` for its loaded HTML document. The document scene is +attached below the web view's clipped scroll container so it renders, receives input, and scrolls as +part of the embedding application tree, but it is not added to `SceneManager` as a top-level scene. + +The application scene and document scene are separate style and resource boundaries: + +- Application stylesheets must not match web-view document nodes. +- Document stylesheets must not match application widgets or sibling web views. +- URI, referer, navigation interception, cookies, dirty style/layout queues, actions, keyframes, and + author `@font-face` aliases are document-scoped state. +- Application code that intentionally injects document CSS must use + `UIWebView::getDocumentSceneNode()->combineStyleSheet(...)`. + +Nested document scenes inherit only host services needed to operate inside the embedding UI: + +- window and DPI, +- shared `UIEventDispatcher`, +- thread pool, +- color-scheme and contrast preferences, +- default font, default font size, and default theme pointer for native controls. + +Do not copy the host stylesheet, URI, referer, navigation callback, cookies, dirty queues, roots, +actions, or icon-theme ownership into a document scene. + +The document scene has two independent sizes: + +- its actual scene extent is content-sized and is the `UIScrollView` scroll target; +- its explicit viewport size is the visible web-view viewport and is used for media queries, + viewport units, HTML/body minimum height, and fixed-position layout. + +Document scenes are owner-updated by `UIWebView::scheduledUpdate()`. Do not register them with +`SceneManager`, and do not add a second update subscription for the same document scene. + +Author `@font-face` family names resolve through the owning `UISceneNode` before the global +`FontManager`. Internally registered font resources use scene-unique names so two documents can +declare the same CSS family without replacing each other. Generic, system, and explicitly shared +application fonts remain global fallbacks. + +Async document work must be generation-guarded. A newer navigation invalidates older document +responses and document-scene resource loads, and destroying the web view or document scene invalidates +pending callbacks before they mutate styles, cookies, fonts, or DOM. + +Inspector and debugging tools should target either the application scene or a document scene +explicitly. Application-root searches intentionally stop at nested scene boundaries; use +`UIWebView::getDocumentSceneNode()` when inspecting or querying document nodes. + ### UIHTMLWidget `UIHTMLWidget` is the base class for HTML-like elements. It stores parsed CSS layout state such as `display`, `position`, `float`, `clear`, list style, and data attributes. It does not own all layout math directly. Instead, it uses `UILayouterManager` to instantiate the appropriate `UILayouter` for its `CSSDisplay`. diff --git a/include/eepp/scene/node.hpp b/include/eepp/scene/node.hpp index 923434ab8..ded70e251 100644 --- a/include/eepp/scene/node.hpp +++ b/include/eepp/scene/node.hpp @@ -29,6 +29,10 @@ class SceneNode; }} // namespace EE::Scene using namespace EE::Scene; +namespace EE { namespace UI { +class UISceneNode; +}} // namespace EE::UI + namespace EE { namespace Scene { /** @@ -1929,6 +1933,7 @@ class EE_API Node : public Transformable { /** @brief Forward declaration for EventDispatcher. */ friend class EventDispatcher; + friend class EE::UI::UISceneNode; std::string mId; String::HashType mIdHash{ 0 }; diff --git a/include/eepp/ui/uiscenenode.hpp b/include/eepp/ui/uiscenenode.hpp index 70b5748ea..4a4235be2 100644 --- a/include/eepp/ui/uiscenenode.hpp +++ b/include/eepp/ui/uiscenenode.hpp @@ -12,6 +12,8 @@ #include #include +#include + using namespace EE::Network; namespace EE { namespace Graphics { @@ -100,6 +102,42 @@ class EE_API UISceneNode : public SceneNode { */ UISceneNode* setPixelsSize( const Float& x, const Float& y ); + /** + * @brief Sets the viewport size used by viewport-relative CSS and media queries. + * + * The viewport size is independent from the scene extent. This is useful for embedded, + * scrollable scenes whose actual size represents their document extent. + * + * @param size The viewport size in pixels. + */ + void setViewportPixelsSize( const Sizef& size ); + + /** Clears the explicit viewport size so the scene extent is used as the viewport. */ + void clearViewportPixelsSize(); + + /** @return The explicit viewport size, or the scene extent when no override is set. */ + const Sizef& getViewportPixelsSize() const; + + /** + * @brief Controls whether a nested scene automatically follows its direct parent's size. + * + * Existing nested scenes follow their parent by default. + */ + void setFollowParentSize( bool followParentSize ); + + /** @return Whether a nested scene automatically follows its direct parent's size. */ + bool followsParentSize() const; + + /** + * @brief Binds an embedded scene to host-scene services without copying document state. + * + * Copies only shared platform/configuration services: dispatcher, DPI/window pointer, + * thread pool, color/contrast preferences, and default font/theme pointers. Stylesheets, + * URI, referer, cookies, navigation callbacks, actions, roots, and dirty queues remain owned + * by this scene. + */ + void initializeEmbeddedFromHost( UISceneNode* hostScene ); + /** * @brief Gets the size in density-independent pixels (dp). * @@ -754,6 +792,10 @@ class EE_API UISceneNode : public SceneNode { Network::CookieManager& getCookieManager() { return mCookieManager; } + void invalidateAsyncResourceLoads(); + + virtual void invalidate( Node* invalidator ); + Font* getFontFromNamesList( std::string_view names, Uint32 fontStyle = 0, FontWeight weight = FontWeight::Normal ) const; @@ -782,6 +824,12 @@ class EE_API UISceneNode : public SceneNode { UIThemeManager* mUIThemeManager{ nullptr }; UIIconThemeManager* mUIIconThemeManager{ nullptr }; std::vector mFontFaces; + UnorderedMap mFontFaceAliases; + struct AsyncResourceLoadState { + bool alive{ true }; + Uint64 generation{ 0 }; + }; + std::shared_ptr mAsyncResourceLoadState; KeyBindings mKeyBindings; std::map mKeyBindingCommands; UnorderedSet mDirtyStyle; @@ -796,6 +844,10 @@ class EE_API UISceneNode : public SceneNode { Node* mCurParent{ nullptr }; Uint32 mCurOnSizeChangeListener{ 0 }; Uint32 mCurrentMarker{ 0 }; + Sizef mViewportPixelsSize; + bool mHasViewportPixelsSize{ false }; + bool mFollowParentSize{ true }; + bool mOwnsEventDispatcher{ true }; std::shared_ptr mThreadPool; URI mURI; URI mReferer; @@ -844,6 +896,12 @@ class EE_API UISceneNode : public SceneNode { */ virtual void onParentChange(); + virtual void onSceneChange(); + + void updateParentSizeListener(); + void onViewportPixelsSizeChange(); + UISceneNode* getHostUISceneNode() const; + /** * @brief Sets the internal pixel size without triggering update cycles. * @@ -957,6 +1015,12 @@ class EE_API UISceneNode : public SceneNode { */ void loadFontFaces( const CSS::StyleSheetStyleVector& styles, URI baseURI = {} ); + void registerFontFaceAlias( std::string_view family, Uint32 fontStyle, FontWeight weight, + Font* font ); + + Font* getFontFaceAlias( std::string_view family, Uint32 fontStyle, + FontWeight weight = FontWeight::Normal ) const; + /** * @brief Loads CSS files from URI * diff --git a/include/eepp/ui/uiscrollview.hpp b/include/eepp/ui/uiscrollview.hpp index ba4ad6610..c8f731653 100644 --- a/include/eepp/ui/uiscrollview.hpp +++ b/include/eepp/ui/uiscrollview.hpp @@ -94,7 +94,7 @@ class EE_API UIScrollView : public UITouchDraggableWidget { void onValueChangeCb( const Event* Event ); - void onScrollViewSizeChange( const Event* Event ); + virtual void onScrollViewSizeChange( const Event* Event ); void onScrollViewPositionChange( const Event* Event ); @@ -113,7 +113,6 @@ class EE_API UIScrollView : public UITouchDraggableWidget { void clearListeners(); void updateInternalSize(); - }; }} // namespace EE::UI diff --git a/include/eepp/ui/uiwebview.hpp b/include/eepp/ui/uiwebview.hpp index bdbfb5af5..158c76a5a 100644 --- a/include/eepp/ui/uiwebview.hpp +++ b/include/eepp/ui/uiwebview.hpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -18,6 +19,7 @@ namespace EE { namespace UI { class UIHTMLHtml; class UIHTMLBody; +class UISceneNode; class EE_API UIWebView : public UIScrollView { public: @@ -67,6 +69,8 @@ class EE_API UIWebView : public UIScrollView { UIWidget* getDocumentContainer() const; + UISceneNode* getDocumentSceneNode() const; + void setStyleSheetDefaultMarker( Uint32 marker ); void setUserAgent( const std::string& userAgent ); @@ -90,10 +94,24 @@ class EE_API UIWebView : public UIScrollView { protected: UIWebView(); + UISceneNode* mDocumentScene{ nullptr }; UIWidget* mDocContainer{ nullptr }; + Uint32 mScrollContainerSizeChangeCb{ 0 }; + Uint32 mVerticalScrollVisibleChangeCb{ 0 }; + Uint32 mHorizontalScrollVisibleChangeCb{ 0 }; + bool mUpdatingDocumentViewportMetrics{ false }; + bool mUpdatingDocumentContentExtent{ false }; + bool mDocumentViewportSyncQueued{ false }; + struct NavigationLoadState { + UIWebView* owner{ nullptr }; + bool alive{ true }; + Uint64 generation{ 0 }; + }; + std::shared_ptr mNavigationLoadState; std::vector mHistory; int mHistoryIndex{ -1 }; bool mIsLoading{ false }; + Uint64 mNavigationGeneration{ 0 }; std::string mUserAgent; Time mDefaultTimeout{ Seconds( 30 ) }; Uint32 mStyleSheetDefaultMarker{ 0 }; @@ -104,16 +122,29 @@ class EE_API UIWebView : public UIScrollView { const Http::Request::FieldTable& headers ); virtual void onSizeChange(); + virtual void onSceneChange(); + virtual void scheduledUpdate( const Time& time ); + virtual void onScrollViewSizeChange( const Event* event ); void loadDocumentData( URI url, std::string data ); + void loadDocumentData( URI url, std::string data, Uint64 generation ); void loadDocumentAsync( const URI& url, const std::string& method = "GET", const std::string& body = "", const Http::Request::FieldTable& headers = Http::Request::FieldTable() ); + Uint64 beginNavigationLoad(); + bool isNavigationLoadCurrent( Uint64 generation ) const; + static UIWebView* resolveNavigationLoad( const std::weak_ptr& state, + Uint64 generation ); void pushHistory( const URI& url ); void navigateToHistoryIndex( int index ); + Sizef getDocumentViewportPixelsSize() const; + void onDocumentViewportGeometryChanged(); void updateHTMLMinHeight( UIHTMLHtml* html, UIHTMLBody* body ); void updateHTMLMinHeightForDocument(); + bool updateDocumentViewportMetrics(); + void updateDocumentSceneContentExtent(); + void updateDocumentSceneMetrics(); }; }} // namespace EE::UI diff --git a/src/eepp/scene/node.cpp b/src/eepp/scene/node.cpp index a5579a93d..5871ebcff 100644 --- a/src/eepp/scene/node.cpp +++ b/src/eepp/scene/node.cpp @@ -795,6 +795,10 @@ const String::HashType& Node::getIdHash() const { return mIdHash; } +static bool isNestedSceneBoundary( const Node* child ) { + return child->isSceneNode() && child->getSceneNode() == child; +} + Node* Node::findIdHash( const String::HashType& idHash ) const { if ( !isClosing() && mIdHash == idHash && !inClosingTree() ) { return const_cast( this ); @@ -802,6 +806,12 @@ Node* Node::findIdHash( const String::HashType& idHash ) const { Node* child = mChild; while ( NULL != child ) { + if ( !child->isClosing() && child->getIdHash() == idHash && !child->inClosingTree() ) + return child; + if ( isNestedSceneBoundary( child ) ) { + child = child->mNext; + continue; + } Node* foundNode = child->findIdHash( idHash ); if ( NULL != foundNode ) @@ -838,6 +848,12 @@ Node* Node::findByType( const Uint32& type ) const { } else { Node* child = mChild; while ( NULL != child ) { + if ( !child->isClosing() && child->isType( type ) && !child->inClosingTree() ) + return child; + if ( isNestedSceneBoundary( child ) ) { + child = child->mNext; + continue; + } Node* foundNode = child->findByType( type ); if ( NULL != foundNode ) return foundNode; @@ -856,6 +872,12 @@ std::vector Node::findAllByType( const Uint32& type ) const { Node* child = mChild; while ( NULL != child ) { + if ( isNestedSceneBoundary( child ) ) { + if ( !child->isClosing() && child->isType( type ) && !child->inClosingTree() ) + nodes.push_back( child ); + child = child->mNext; + continue; + } std::vector foundNode = child->findAllByType( type ); if ( !foundNode.empty() ) nodes.insert( nodes.end(), foundNode.begin(), foundNode.end() ); diff --git a/src/eepp/ui/css/mediaquery.cpp b/src/eepp/ui/css/mediaquery.cpp index f495b43b4..20969c652 100644 --- a/src/eepp/ui/css/mediaquery.cpp +++ b/src/eepp/ui/css/mediaquery.cpp @@ -103,7 +103,7 @@ MediaQuery::ptr MediaQuery::parse( const std::string& str ) { query->mExpressions.push_back( expr ); } } - } else { + } else if ( tok != "only" && tok != "and" ) { query->mMediaType = (MediaType)String::valueIndex( tok, MediaTypeStrings, media_type_all ); } diff --git a/src/eepp/ui/uinode.cpp b/src/eepp/ui/uinode.cpp index 382fb2dd0..173a7da02 100644 --- a/src/eepp/ui/uinode.cpp +++ b/src/eepp/ui/uinode.cpp @@ -1820,8 +1820,10 @@ Float UINode::convertLength( const CSS::StyleSheetLength& length, if ( getUISceneNode() && getUISceneNode()->getUIThemeManager() ) font = getUISceneNode()->getUIThemeManager()->getDefaultFont(); - auto ret = length.asPixels( containerLength, getSceneNode()->getPixelsSize(), - getSceneNode()->getDPI(), elFontSize, rootFontSize, font ); + const Sizef& viewportSize = getUISceneNode() ? getUISceneNode()->getViewportPixelsSize() + : getSceneNode()->getPixelsSize(); + auto ret = length.asPixels( containerLength, viewportSize, getSceneNode()->getDPI(), elFontSize, + rootFontSize, font ); if ( ( mFlags & UI_HTML_ELEMENT ) && length.getUnit() == StyleSheetLength::Unit::Px ) ret = PixelDensity::dpToPx( ret ); // scale px as if where dp in HTML elements diff --git a/src/eepp/ui/uiscenenode.cpp b/src/eepp/ui/uiscenenode.cpp index a28d75117..8b630b984 100644 --- a/src/eepp/ui/uiscenenode.cpp +++ b/src/eepp/ui/uiscenenode.cpp @@ -73,6 +73,7 @@ UISceneNode::UISceneNode( EE::Window::Window* window ) : mUpdatingLayouts( false ), mUIThemeManager( UIThemeManager::New() ), mUIIconThemeManager( UIIconThemeManager::New()->setFallbackThemeManager( mUIThemeManager ) ), + mAsyncResourceLoadState( std::make_shared() ), mKeyBindings( mWindow->getInput() ) { // Reset size since the SceneNode already set it but needs to set the size from zero to emit // the required events to its children. @@ -94,6 +95,11 @@ UISceneNode::UISceneNode( EE::Window::Window* window ) : } UISceneNode::~UISceneNode() { + if ( mAsyncResourceLoadState ) { + mAsyncResourceLoadState->alive = false; + mAsyncResourceLoadState->generation++; + } + eeSAFE_DELETE( mUIThemeManager ); eeSAFE_DELETE( mUIIconThemeManager ); @@ -105,6 +111,9 @@ UISceneNode::~UISceneNode() { // We need to ensure that the children are destroyed before the thread pool, // since its children could be consuming it and need to uninitialize gracefully. childDeleteAll(); + + if ( !mOwnsEventDispatcher ) + mEventDispatcher = nullptr; } void UISceneNode::resizeNode( EE::Window::Window* ) { @@ -164,22 +173,84 @@ void UISceneNode::onParentChange() { if ( mCurParent && mCurOnSizeChangeListener ) mCurParent->removeEventListener( mCurOnSizeChangeListener ); + mCurOnSizeChangeListener = 0; - if ( !mCurParent ) + if ( !mCurParent && mOwnsEventDispatcher ) eeSAFE_DELETE( mEventDispatcher ); mCurParent = mParentNode; if ( !mParentNode ) { setEventDispatcher( UIEventDispatcher::New( this ) ); + mOwnsEventDispatcher = true; return; } - mEventDispatcher = getParent()->asType()->getUISceneNode()->getEventDispatcher(); + initializeEmbeddedFromHost( getHostUISceneNode() ); setDirty(); - setPixelsSize( getParent()->getPixelsSize() ); + updateParentSizeListener(); +} +void UISceneNode::onSceneChange() { + mSceneNode = this; + eeASSERT( !removeFromCloseQueue( this ) ); + initializeEmbeddedFromHost( getHostUISceneNode() ); + + Node* child = getFirstChild(); + while ( NULL != child ) { + child->onSceneChange(); + child = child->getNextNode(); + } +} + +UISceneNode* UISceneNode::getHostUISceneNode() const { + const Node* node = getParent(); + while ( node ) { + SceneNode* sceneNode = node->getSceneNode(); + if ( sceneNode && sceneNode->isUISceneNode() && sceneNode != this ) + return static_cast( sceneNode ); + if ( node->isUISceneNode() && node != this ) + return const_cast( node )->asType(); + node = node->getParent(); + } + return nullptr; +} + +void UISceneNode::initializeEmbeddedFromHost( UISceneNode* hostScene ) { + if ( !hostScene || hostScene == this ) + return; + + mWindow = hostScene->getWindow(); + mDPI = hostScene->getDPI(); + mEventDispatcher = hostScene->getEventDispatcher(); + mOwnsEventDispatcher = false; + mThreadPool = hostScene->getThreadPool(); + mColorSchemePreference = hostScene->getColorSchemePreference(); + mContrastPreference = hostScene->getContrastPreference(); + + UIThemeManager* hostThemeManager = hostScene->getUIThemeManager(); + if ( hostThemeManager ) { + mUIThemeManager->setDefaultFont( hostThemeManager->getDefaultFont() ); + mUIThemeManager->setDefaultFontSize( hostThemeManager->getDefaultFontSize() ); + mUIThemeManager->setDefaultTheme( hostThemeManager->getDefaultTheme() ); + mUIThemeManager->setAutoApplyDefaultTheme( hostThemeManager->getAutoApplyDefaultTheme() ); + mUIThemeManager->setDefaultEffectsEnabled( hostThemeManager->getDefaultEffectsEnabled() ); + mUIThemeManager->setWidgetsFadeInTime( hostThemeManager->getWidgetsFadeInTime() ); + mUIThemeManager->setWidgetsFadeOutTime( hostThemeManager->getWidgetsFadeOutTime() ); + mUIThemeManager->setTooltipTimeToShow( hostThemeManager->getTooltipTimeToShow() ); + mUIThemeManager->setTooltipFollowMouse( hostThemeManager->getTooltipFollowMouse() ); + mUIThemeManager->setCursorSize( hostThemeManager->getCursorSize() ); + } + + mUIIconThemeManager->setFallbackThemeManager( mUIThemeManager ); +} + +void UISceneNode::updateParentSizeListener() { + if ( !mParentNode || !mFollowParentSize ) + return; + + setPixelsSize( getParent()->getPixelsSize() ); mCurOnSizeChangeListener = getParent()->on( Event::OnSizeChange, [this]( const Event* ) { setDirty(); setPixelsSize( getParent()->getPixelsSize() ); @@ -712,6 +783,50 @@ UISceneNode* UISceneNode::setPixelsSize( const Float& x, const Float& y ) { return setPixelsSize( Sizef( x, y ) ); } +void UISceneNode::setViewportPixelsSize( const Sizef& size ) { + if ( mHasViewportPixelsSize && mViewportPixelsSize == size ) + return; + + mViewportPixelsSize = size; + mHasViewportPixelsSize = true; + mRoot->setPixelsSize( mViewportPixelsSize ); + onViewportPixelsSizeChange(); +} + +void UISceneNode::clearViewportPixelsSize() { + if ( !mHasViewportPixelsSize ) + return; + + mHasViewportPixelsSize = false; + onViewportPixelsSizeChange(); +} + +void UISceneNode::onViewportPixelsSizeChange() { + onMediaChanged(); + reloadStyle( true, true, true ); + sendMsg( this, NodeMessage::WindowResize ); +} + +const Sizef& UISceneNode::getViewportPixelsSize() const { + return mHasViewportPixelsSize ? mViewportPixelsSize : getPixelsSize(); +} + +void UISceneNode::setFollowParentSize( bool followParentSize ) { + if ( mFollowParentSize == followParentSize ) + return; + + mFollowParentSize = followParentSize; + if ( mCurParent && mCurOnSizeChangeListener ) { + mCurParent->removeEventListener( mCurOnSizeChangeListener ); + mCurOnSizeChangeListener = 0; + } + updateParentSizeListener(); +} + +bool UISceneNode::followsParentSize() const { + return mFollowParentSize; +} + void UISceneNode::update( const Time& elapsed ) { UISceneNode* uiSceneNode = SceneManager::instance()->getUISceneNode(); @@ -1044,11 +1159,12 @@ Drawable* UISceneNode::findIconDrawable( const std::string& iconName, const size CSS::MediaFeatures UISceneNode::getMediaFeatures() const { CSS::MediaFeatures media; + const Sizef& viewportSize = getViewportPixelsSize(); media.type = media_type_screen; - media.width = mWindow->getWidth(); - media.height = mWindow->getHeight(); - media.deviceWidth = mWindow->getDesktopResolution().getWidth(); - media.deviceHeight = mWindow->getDesktopResolution().getHeight(); + media.width = PixelDensity::pxToDp( viewportSize.getWidth() ); + media.height = PixelDensity::pxToDp( viewportSize.getHeight() ); + media.deviceWidth = PixelDensity::pxToDp( mWindow->getDesktopResolution().getWidth() ); + media.deviceHeight = PixelDensity::pxToDp( mWindow->getDesktopResolution().getHeight() ); media.color = 8; media.monochrome = 0; media.colorIndex = 256; @@ -1079,7 +1195,7 @@ void UISceneNode::onChildCountChange( Node* child, const bool& removed ) { void UISceneNode::onSizeChange() { SceneNode::onSizeChange(); - mRoot->setPixelsSize( getPixelsSize() ); + mRoot->setPixelsSize( getViewportPixelsSize() ); } void UISceneNode::processStyleSheetAtRules( const StyleSheet& styleSheet, URI baseURI ) { @@ -1180,23 +1296,72 @@ void UISceneNode::resolveStyleSheetRelativeURLs( CSS::StyleSheet& styleSheet, UR } } -void UISceneNode::loadFontFaces( const StyleSheetStyleVector& styles, URI baseURI ) { - auto loadFont = [this, baseURI]( const std::string& familyName, - const CSS::StyleSheetProperty& srcProp, Font* fontFamily, - Uint32 fontStyle ) { - auto trySetFontFamily = []( Font* fontFamily, Uint32 fontStyle, FontTrueType* font ) { - if ( fontFamily && fontFamily->getType() == FontType::TTF && fontStyle ) { - FontTrueType* ttf = static_cast( fontFamily ); - if ( fontStyle == ( Text::Italic | Text::Bold ) ) { - ttf->setBoldItalicFont( font ); - } else if ( fontStyle == Text::Italic ) { - ttf->setItalicFont( font ); - } else if ( fontStyle == Text::Bold ) { - ttf->setBoldFont( font ); - } - } - }; +static std::string normalizeFontFaceFamily( std::string_view family ) { + std::string normalized{ String::trim( family, ' ' ) }; + normalized = String::trim( normalized, '\'' ); + normalized = String::trim( normalized, '"' ); + return String::toLower( normalized ); +} +static std::string makeFontFaceAliasKey( std::string_view family, Uint32 fontStyle, + FontWeight weight ) { + FontWeight resolvedWeight = + weight != FontWeight::Normal + ? weight + : ( ( fontStyle & Text::Bold ) ? FontWeight::Bold : FontWeight::Normal ); + return String::format( "%s#%u#%u", normalizeFontFaceFamily( family ).c_str(), fontStyle, + static_cast( resolvedWeight ) ); +} + +void UISceneNode::registerFontFaceAlias( std::string_view family, Uint32 fontStyle, + FontWeight weight, Font* font ) { + if ( family.empty() || font == nullptr || !font->loaded() ) + return; + + mFontFaceAliases[makeFontFaceAliasKey( family, fontStyle, weight )] = font; +} + +Font* UISceneNode::getFontFaceAlias( std::string_view family, Uint32 fontStyle, + FontWeight weight ) const { + auto aliasIt = mFontFaceAliases.find( makeFontFaceAliasKey( family, fontStyle, weight ) ); + if ( aliasIt != mFontFaceAliases.end() ) + return aliasIt->second; + + if ( weight != FontWeight::Normal ) { + aliasIt = + mFontFaceAliases.find( makeFontFaceAliasKey( family, fontStyle, FontWeight::Normal ) ); + if ( aliasIt != mFontFaceAliases.end() ) + return aliasIt->second; + } + + if ( fontStyle != 0 ) { + aliasIt = mFontFaceAliases.find( makeFontFaceAliasKey( family, 0, weight ) ); + if ( aliasIt != mFontFaceAliases.end() ) + return aliasIt->second; + } + + return nullptr; +} + +void UISceneNode::loadFontFaces( const StyleSheetStyleVector& styles, URI baseURI ) { + auto loadFont = [this, baseURI]( const std::string& authorFamily, + const CSS::StyleSheetProperty& srcProp, Uint32 fontStyle, + FontWeight fontWeight ) { + auto makeInternalFontName = [this]( const std::string& familyName, Uint32 fontStyle, + FontWeight fontWeight ) { + return String::format( "__eepp_font_face_%p_%zu_%s_%u_%u", this, + mFontFaces.size() + mFontFaceAliases.size(), familyName.c_str(), + fontStyle, static_cast( fontWeight ) ); + }; + auto registerLoadedFont = [this, authorFamily, fontStyle, + fontWeight]( FontTrueType* font ) { + if ( font == nullptr || !font->loaded() ) + return false; + registerFontFaceAlias( authorFamily, fontStyle, fontWeight, font ); + mFontFaces.push_back( font ); + mRoot->reloadFontFamily(); + return true; + }; std::string path( srcProp.getValue() ); FunctionString func( FunctionString::parse( path ) ); @@ -1219,13 +1384,10 @@ void UISceneNode::loadFontFaces( const StyleSheetStyleVector& styles, URI baseUR if ( isBase64 && !data.empty() ) { std::string decoded; Base64::decode( data, decoded ); - FontTrueType* font = FontTrueType::New( familyName ); + FontTrueType* font = FontTrueType::New( + makeInternalFontName( authorFamily, fontStyle, fontWeight ) ); if ( font->loadFromMemory( &decoded[0], decoded.size() ) ) { - runOnMainThread( [this, trySetFontFamily, fontFamily, fontStyle, font] { - trySetFontFamily( fontFamily, fontStyle, font ); - mFontFaces.push_back( font ); - mRoot->reloadFontFamily(); - } ); + registerLoadedFont( font ); } else eeSAFE_DELETE( font ); } @@ -1238,46 +1400,55 @@ void UISceneNode::loadFontFaces( const StyleSheetStyleVector& styles, URI baseUR if ( String::startsWith( path, "file://" ) ) { std::string filePath( path.substr( 7 ) ); - FontTrueType* font = FontTrueType::New( familyName ); + FontTrueType* font = + FontTrueType::New( makeInternalFontName( authorFamily, fontStyle, fontWeight ) ); - font->loadFromFile( filePath ); - trySetFontFamily( fontFamily, fontStyle, font ); - - mFontFaces.push_back( font ); - runOnMainThread( [this] { mRoot->reloadFontFamily(); } ); + if ( font->loadFromFile( filePath ) ) { + registerLoadedFont( font ); + runOnMainThread( [this] { mRoot->reloadFontFamily(); } ); + } else + eeSAFE_DELETE( font ); } else if ( String::startsWith( path, "http://" ) || String::startsWith( path, "https://" ) ) { + std::string internalFontName( + makeInternalFontName( authorFamily, fontStyle, fontWeight ) ); + auto resourceState = mAsyncResourceLoadState; + Uint64 resourceGeneration = resourceState ? resourceState->generation : 0; Http::getAsync( - [this, fontStyle, familyName, fontFamily, trySetFontFamily, + [this, resourceState, resourceGeneration, internalFontName, registerLoadedFont, path]( const Http&, Http::Request&, Http::Response& response ) { - FontTrueType* font = FontTrueType::New( familyName ); + if ( !resourceState || !resourceState->alive || + resourceState->generation != resourceGeneration ) + return; + FontTrueType* font = FontTrueType::New( internalFontName ); if ( response.isOK() && !response.getBody().empty() && font->loadFromMemory( &response.getBody()[0], response.getBody().size() ) ) { - runOnMainThread( [this, font, fontFamily, fontStyle, trySetFontFamily] { - trySetFontFamily( fontFamily, fontStyle, font ); - mFontFaces.push_back( font ); - mRoot->reloadFontFamily(); + runOnMainThread( [resourceState, resourceGeneration, registerLoadedFont, + font]() mutable { + if ( resourceState && resourceState->alive && + resourceState->generation == resourceGeneration ) { + registerLoadedFont( font ); + } else { + eeSAFE_DELETE( font ); + } } ); } else { eeSAFE_DELETE( font ); Log::error( "UISceneNode::loadFontFaces: Failed to load font \"%s\", from: " "%s. Request response status code: %d (%s)", - familyName, path, response.getStatus(), + internalFontName, path, response.getStatus(), response.getStatusDescription() ); } }, URI( path ), Seconds( 5 ) ); } else if ( VFS::instance()->fileExists( path ) ) { - FontTrueType* font = FontTrueType::New( familyName ); + FontTrueType* font = + FontTrueType::New( makeInternalFontName( authorFamily, fontStyle, fontWeight ) ); IOStream* stream = VFS::instance()->getFileFromPath( path ); if ( font->loadFromStream( *stream ) ) { - runOnMainThread( [this, fontFamily, fontStyle, font, trySetFontFamily] { - trySetFontFamily( fontFamily, fontStyle, font ); - mFontFaces.push_back( font ); - mRoot->reloadFontFamily(); - } ); + registerLoadedFont( font ); } else eeSAFE_DELETE( font ); } @@ -1292,6 +1463,9 @@ void UISceneNode::loadFontFaces( const StyleSheetStyleVector& styles, URI baseUR Uint32 fontStyle = fontStyleProp ? fontStyleProp->asFontStyle() : 0; auto fontWeightProp = style->getPropertyById( PropertyId::FontWeight ); fontStyle |= fontWeightProp ? fontWeightProp->asFontStyle() : 0; + FontWeight fontWeight = fontWeightProp + ? Text::stringToFontWeight( fontWeightProp->getValue() ) + : FontWeight::Normal; CSS::StyleSheetProperty familyProp( *family ); CSS::StyleSheetProperty srcProp( *src ); @@ -1299,13 +1473,9 @@ void UISceneNode::loadFontFaces( const StyleSheetStyleVector& styles, URI baseUR if ( familyProp.isEmpty() || srcProp.isEmpty() ) return; - Font* fontSearch = FontManager::instance()->getByName( familyProp.getValue() ); std::string fontFamily( String::trim( familyProp.getValue(), '"' ) ); String::trimInPlace( fontFamily, "'" ); - if ( fontStyle ) - fontFamily += "#" + Text::styleFlagToString( fontStyle ); - auto unicodeRange = style->getPropertyById( PropertyId::UnicodeRange ); // We don't support unicode ranges yet @@ -1313,11 +1483,7 @@ void UISceneNode::loadFontFaces( const StyleSheetStyleVector& styles, URI baseUR continue; } - if ( nullptr == fontSearch ) { - loadFont( fontFamily, srcProp, nullptr, fontStyle ); - } else if ( fontSearch->isRegular() && fontSearch->getFontStyle() != fontStyle ) { - loadFont( fontFamily, srcProp, fontSearch, fontStyle ); - } + loadFont( fontFamily, srcProp, fontStyle, fontWeight ); } } @@ -1380,12 +1546,22 @@ void UISceneNode::loadCSS( URI uri, std::optional