Added UISvg (for embedded svg in XML) and UIHTMLImage (to handle HTML img, although still WIP).

This commit is contained in:
Martín Lucas Golini
2026-04-27 16:41:37 -03:00
parent 939ce02a3a
commit 24cf3ad6c7
9 changed files with 1260 additions and 15 deletions

View File

@@ -0,0 +1,555 @@
# Inline SVG Support & HTML Image Element Analysis Plan
This document outlines the architectural plan for adding inline `<svg>` HTML element support and analyzes whether a dedicated `UIHTMLImage` class is needed to improve `<img>` element behavior.
**AGENT DIRECTIVE:** You are Negen. Follow this plan iteratively. Compile and run unit tests after every step. Do NOT proceed if any regression is detected. Take git stash snapshots (`git stash push -m "Phase X.Y passed" && git stash apply`) on passing checkpoints.
---
## Part A: Current State Analysis
### A.1 How SVG Files Load via `<img src="file.svg">` Today
```
HTML: <img src="image.svg">
→ UIWidgetCreator::createFromName("img") → UIImage
→ UIImage::loadFromXmlNode → UIImage::applyProperty(PropertyId::Src)
→ DrawableImageParser::createDrawable(path) / DrawableSearcher::searchByName(path)
→ resolves file://, http://, data: URI
→ TextureFactory::loadFromFile/Memory → Image() → detects .svg extension
→ Image::svgLoad() → nanosvg parse + rasterize → RGBA pixels → Texture (GPU)
→ UIImage::setDrawable(texture) → draw() renders via OpenGL at widget size
```
### A.2 Why `<svg>` Inline Elements Fail Today
1. `UIWidgetCreator` has no `"svg"` registration (line 164 of widgetcreator.cpp)
2. When the HTML parser encounters `<svg>...</svg>`, it calls `createFromName("svg")` → returns `nullptr` → silently skipped
3. Even if a widget were created, the SVG's **children** (`<circle>`, `<rect>`, `<path>`, etc.) would be recursively loaded as HTML/UI widgets by the parent (since `loadsItsChildren()` would return false) — this would pollute the widget tree with garbage null lookups
### A.3 Existing SVG Infrastructure We Can Reuse
| Component | File | Role |
|---|---|---|
| nanosvg parser | `src/thirdparty/nanosvg/nanosvg.h` | Parses SVG XML to `NSVGimage` (paths, paints, gradients) |
| nanosvg rasterizer | `src/thirdparty/nanosvg/nanosvgrast.h` | Rasterizes to RGBA pixel buffer |
| `Image::svgLoad()` | `src/eepp/graphics/image.cpp:1008` | Parses + rasterizes SVG in a single call |
| `Image::getInfoFromMemory()` | `include/eepp/graphics/image.hpp:183` | Reads SVG intrinsic width/height without rasterizing |
| `TextureFactory::loadFromMemory()` | `include/eepp/graphics/texturefactory.hpp:77` | Creates GPU Texture from raw pixel data with `FormatConfiguration` (including `svgScale`) |
| `UISVGIcon` class | `include/eepp/ui/uiicon.hpp:50` | Rasterizes SVG XML on-demand at requested size (icons only) |
| `UIImage` class | `include/eepp/ui/uiimage.hpp` | Drawable-based rendering with scale types, alignment, tinting, aspect-ratio-preserving auto-sizing |
| `DrawableSearcher::searchByName()` | `src/eepp/graphics/drawablesearcher.cpp` | Handles `data:image/svg+xml,...` URIs in CSS `url()` |
| `UISceneNode::getThreadPool()` | `src/eepp/ui/uiscenenode.cpp:470` | Thread pool for async operations |
| `UIImageViewer::loadImageAsync()` | `src/eepp/ui/tools/uiimageviewer.cpp:100` | Proven async image loading pattern (thread pool + Sprite ownership) |
### A.4 Key Class Hierarchy (What UISvg Needs to Fit Into)
```
UINode
└── UIWidget ← default SizePolicy::WrapContent (width + height)
├── UIImage ← mDrawable, mScaleType, mColor, onAutoSize(), calcDestSize(), draw()
│ └── UISvg (NEW) ← our new class
└── UILayout
└── UIHTMLWidget ← CSSDisplay, CSSPosition, layouter integration
├── UIRichText ← rebuildRichText() processes inline/block children
└── UITextSpan
```
**Key insight:** `UISvg` inherits from `UIImage` (not `UIHTMLWidget`). This means:
- Reuses all drawing, scaling, and alignment code
- Does NOT participate in the CSS display/position system (treated as a "custom" widget in rich text flow)
- In `rebuildRichText()`, it's classified as `isBlock` only if `mWidthPolicy == MatchParent`, otherwise inline — which matches HTML's default inline-block behavior for `<svg>`
---
## Part B: Implementation Plan — UISvg Widget
### Phase 1: Core UISvg Class
#### Step 1.1: Add `UI_TYPE_SVG` to UINodeType Enum
**File:** `include/eepp/ui/uihelper.hpp`
Insert `UI_TYPE_SVG` after `UI_TYPE_HTML_LIST_ITEM` (line 131), before `UI_TYPE_MODULES`:
```cpp
UI_TYPE_HTML_LIST_ITEM,
UI_TYPE_SVG, // NEW
UI_TYPE_MODULES = 10000,
```
#### Step 1.2: Create UISvg Header
**File:** `include/eepp/ui/uisvg.hpp` (NEW)
```cpp
#ifndef EE_UI_UISVG_HPP
#define EE_UI_UISVG_HPP
#include <eepp/ui/uiimage.hpp>
namespace EE { namespace UI {
class EE_API UISvg : public UIImage {
public:
static UISvg* New();
virtual ~UISvg();
virtual Uint32 getType() const;
virtual bool isType( const Uint32& type ) const;
virtual void loadFromXmlNode( const pugi::xml_node& node );
const std::string& getSvgXml() const;
protected:
UISvg();
void onSizeChange() override;
std::string mSvgXml;
Uint64 mTag{ 0 }; // async task tag for cleanup on destruction
static const Action::UniqueID sRasterizeId;
void loadSvgXml( const pugi::xml_node& node );
void scheduleRasterize();
void rasterizeSvg( const std::string& svgXml );
void clearThreadTag();
};
}} // namespace EE::UI
#endif
```
**Design decisions:**
- Inherits from `UIImage` (not `UIHTMLWidget`) — simpler, reuses all rendering/scaling/alignment code
- Stores raw SVG XML in `mSvgXml` for re-rasterization when the widget resizes
- Overrides `loadFromXmlNode` to capture the SVG subtree and trigger rasterization
- Overrides `onSizeChange` to schedule async re-rasterization with debounce
- `getType()` returns `UI_TYPE_SVG` for type-checking (e.g., `widget->isType(UI_TYPE_SVG)`)
- Thread pool task tag stored in `mTag` for cleanup in destructor
#### Step 1.3: Create UISvg Implementation
**File:** `src/eepp/ui/uisvg.cpp` (NEW)
**Constructor:**
```cpp
UISvg::UISvg() : UIImage() {
// Prevent parent from recursively loading SVG children as UI widgets
mFlags |= UI_LOADS_ITS_CHILDREN;
}
```
**Destructor:**
```cpp
UISvg::~UISvg() {
clearThreadTag();
}
```
**loadFromXmlNode override:**
```cpp
void UISvg::loadFromXmlNode( const pugi::xml_node& node ) {
// Process regular attributes (style, id, class, width, height, etc.)
beginAttributesTransaction();
UIWidget::loadFromXmlNode( node );
endAttributesTransaction();
// Serialize the <svg> subtree to string
loadSvgXml( node );
// Kick off async rasterization
scheduleRasterize();
}
```
**XML serialization helper:**
```cpp
// Simple pugi::xml_writer that accumulates into a std::string
class XmlStringWriter : public pugi::xml_writer {
public:
std::string result;
virtual void write( const void* data, size_t size ) override {
result.append( static_cast<const char*>( data ), size );
}
};
void UISvg::loadSvgXml( const pugi::xml_node& node ) {
XmlStringWriter writer;
node.print( writer );
mSvgXml = writer.result;
}
```
**Async rasterization schedule (initial load + size changes):**
```cpp
void UISvg::scheduleRasterize() {
if ( mSvgXml.empty() )
return;
auto size = getPixelsSize();
if ( size.getWidth() <= 0.f || size.getHeight() <= 0.f )
return;
if ( !getUISceneNode()->hasThreadPool() )
return;
clearThreadTag();
std::string svgXml( mSvgXml );
auto pixelDensity = PixelDensity::getPixelDensity();
mTag = getUISceneNode()->getThreadPool()->run(
[this, svgXml = std::move( svgXml ), pixelDensity] {
rasterizeSvg( svgXml );
},
[]( const Uint64& ) {},
(Uint64)this ); // tag by `this` pointer to allow cancelling
}
```
**Rasterization (runs on thread pool):**
```cpp
void UISvg::rasterizeSvg( const std::string& svgXml ) {
Image::FormatConfiguration format;
format.svgScale( PixelDensity::getPixelDensity() );
// Determine target pixel size for rasterization:
// Use the widget's content size at pixel density, or intrinsic SVG size
Texture* texture = TextureFactory::instance()->loadFromMemory(
(const unsigned char*)svgXml.data(), svgXml.size(),
false, // mipmap
Texture::ClampMode::ClampToEdge, // clamp mode
false, false, // compress, keepLocalCopy
format );
if ( !texture )
return;
// Wrap in Sprite to handle TextureFactory ownership lifecycle properly.
// Sprite will remove the texture from TextureFactory and delete it when
// destroyed. UIImage takes ownership of the Sprite via setDrawable(true).
Sprite* sprite = Sprite::New();
sprite->createStatic( texture );
sprite->setAsTextureOwner( true );
sprite->setAsTextureRegionOwner( true );
runOnMainThread( [this, sprite] {
// Use the widget's content size to compute the correct drawable scale.
// The actual SVG intrinsic size determines the drawable's pixel dimensions,
// while the widget's layout size determines the on-screen display bounds.
setDrawable( sprite, true ); // UISvg owns the Sprite → Sprite owns the Texture
} );
}
```
**onSizeChange override (debounced re-rasterization):**
```cpp
void UISvg::onSizeChange() {
UIImage::onSizeChange();
auto size = getPixelsSize();
if ( size.getWidth() <= 0.f || size.getHeight() <= 0.f )
return;
// Debounce: cancel any pending rasterization and schedule a new one.
// Node::debounce() automatically cancels the previous call with the same
// uniqueIdentifier if called again before the delay expires.
debounce( [this] { scheduleRasterize(); },
Milliseconds( 150 ),
sRasterizeId );
}
// In the .cpp file:
const Action::UniqueID UISvg::sRasterizeId = String::hash( "UISvg_rasterize" );
```
**Thread tag cleanup:**
```cpp
void UISvg::clearThreadTag() {
if ( mTag != 0 && getUISceneNode()->hasThreadPool() ) {
getUISceneNode()->getThreadPool()->removeWithTag( (Uint64)this );
mTag = 0;
}
}
```
**Important notes on `UI_LOADS_ITS_CHILDREN`:**
- Setting this flag tells `UIRichText::loadFromXmlNode()` and `UISceneNode::loadNode()` to skip recursive child processing for the SVG node
- Without this flag, the parent would try to create widgets for `<circle>`, `<rect>`, `<path>` etc. — all of which are unknown to `UIWidgetCreator` and would fail silently (but still waste cycles)
- The SVG is NOT expected to contain child elements that should become UI widgets
#### Step 1.4: Register `"svg"` in UIWidgetCreator
**File:** `src/eepp/ui/uiwidgetcreator.cpp`
Add after the existing `"img"` registration (line 168):
```cpp
registeredWidget["svg"] = [] {
auto svg = UISvg::New();
svg->setFlags( UI_HTML_ELEMENT );
return svg;
};
```
This makes `<svg>...</svg>` elements in HTML content create `UISvg` widgets flagged as HTML elements (so the rich text engine treats them appropriately).
#### Step 1.5: Update Makefiles (premake4)
Since we added new `.hpp` and `.cpp` files, regenerate makefiles:
```
premake4 --disable-static-build --with-mold-linker --with-debug-symbols --address-sanitizer gmake
```
**Validation:** Run `make -C make/linux -j$(nproc)` and ensure clean compile. (Snapshot)
#### Step 1.6: Unit Test
**File:** `src/tests/unit_tests/` (specific file TBD, likely create `htmlsvg.cpp` or extend existing HTML tests)
Test at minimum:
1. **Basic inline SVG rendering:** `<svg width="100" height="100"><circle cx="50" cy="50" r="40" fill="red"/></svg>`
2. **SVG with viewBox:** `<svg viewBox="0 0 200 200"><rect width="100" height="100" fill="blue"/></svg>`
3. **CSS sizing on SVG:** `<svg style="width: 200px; height: 150px;">...</svg>`
4. **SVG with xmlns:** `<svg xmlns="http://www.w3.org/2000/svg">...</svg>`
5. **Verification that SVG children are NOT created as UI widgets**
6. **Resize re-rasterization:** Verify the SVG re-renders crisply after resizing the widget
Reference existing SVG test asset: `bin/unit_tests/assets/html/triangle.svg`
**Validation:** Run `ASAN_OPTIONS=detect_leaks=0 xvfb-run -a -s "-screen 0 1280x1024x24" bin/unit_tests/eepp-unit_tests-debug --filter="Svg"` — must pass. (Snapshot)
---
### Phase 2: Edge Cases & Polish
#### Step 2.1: Handle SVG Without Intrinsic Dimensions
Some SVGs lack explicit `width`/`height` attributes. In this case, fall back to the widget's content size or a reasonable default (e.g., 300×150, matching browser defaults for replaced elements).
#### Step 2.2: Handle SVG with viewBox Only
When the SVG has a `viewBox` attribute (e.g., `viewBox="0 0 200 150"`) but no `width`/`height`, the intrinsic aspect ratio should come from the viewBox dimensions. Nanosvg's `Image::getInfoFromMemory` handles this.
#### Step 2.3: HiDPI / Pixel Density
The `svgScale` in `Image::FormatConfiguration` handles this:
- `format.svgScale( PixelDensity::getPixelDensity() )`
- For a device with 2× pixel density, the SVG renders at 2× pixel resolution
- The widget's logical size remains in CSS pixel units
This is correctly handled in the rasterization code above.
#### Step 2.4: SVG with Internal `<style>` / CSS
Nanosvg supports inline styles and the `<style>` element. No special handling needed — the SVG XML serialization preserves all content.
#### Step 2.5: SVG with `<use>` / External References
Nanosvg may not fully support external references (xlink). This is a known limitation inherited from nanosvg, not from our implementation. Document as a known limitation.
#### Step 2.6: Sync Fallback When Thread Pool Unavailable
When `!hasThreadPool()`, rasterize synchronously on the main thread directly in `scheduleRasterize()`. This ensures the SVG still renders in environments without a thread pool.
---
## Part C: UIHTMLImage Analysis & Recommendation
### C.1 Current `UIImage` Behavior as `<img>` Element
| Aspect | Current Implementation | HTML Spec Behavior | Match? |
|---|---|---|---|
| Intrinsic sizing | `onAutoSize()` uses drawable dimensions | Replaced element intrinsic dimensions | ✓ |
| CSS `width: 200px` only | Height auto-computed from aspect ratio | Same | ✓ |
| CSS `height: 200px` only | Width auto-computed from aspect ratio | Same | ✓ |
| Both WrapContent | Sizes to drawable dimensions | Same | ✓ |
| Max-width constraint | Respected in `onAutoSize()` | Same | ✓ |
| `scale-type` | `FitInside`/`Expand`/`None` | Maps to `object-fit` (approximated) | ≈ |
| `text-align` | Used for horizontal alignment | CSS `text-align` on inline elements | ✓ |
| Default display flow | Inline (WrapContent width → `isBlock=false`) | Inline-block | ≈ |
| `alt` attribute | Registered as `tooltip` alias (tooltip text only) | Text fallback when image fails to load | ✗ |
| HTML `width`/`height` attrs | Treated as CSS width/height (Fixed policy) | Presentational hints separate from CSS | ≈ |
| `srcset`/`sizes` | Not supported | Responsive images | ✗ |
| `loading="lazy"` | Not supported | Deferred loading | ✗ |
### C.2 Note on `alt` Attribute
The `alt` attribute is already registered as a tooltip alias in `propertydefinition.cpp`:
```
registerProperty( "tooltip", "" )
.setType( PropertyType::String )
.addAlias( "alt" );
```
This means that currently `<img alt="My Image">` simply sets a tooltip on the widget. It does NOT provide the HTML-spec fallback behavior (showing alt text when the image fails to load). Any UIHTMLImage implementation would need to separately handle the visual alt-text fallback.
### C.3 Gap Analysis
The most impactful gap is the **`alt` attribute fallback display**: when an image fails to load (e.g., broken URL), there's no visible indicator. Everything else is either already handled or an advanced feature.
The sizing behavior is already close to the HTML spec for common use cases. The default `WrapContent` policy on `UIWidget` ensures images display at their intrinsic size unless overridden by CSS, and aspect-ratio preservation works when only one dimension is specified.
### C.4 Recommendation: Create UIHTMLImage (Phase 3)
**Verdict: YES, create a dedicated `UIHTMLImage : public UIImage` class.** Reason:
1. Adding `alt` text fallback display is the most immediate improvement and justifies the class
2. It provides a clean extension point for future HTML-specific image features
3. It separates concerns: HTML semantics can evolve without touching `UIImage`'s general-purpose code
4. Low-risk: it's a thin wrapper with one added feature
#### UIHTMLImage Class Design:
```cpp
class EE_API UIHTMLImage : public UIImage {
public:
static UIHTMLImage* New();
virtual Uint32 getType() const;
virtual bool isType( const Uint32& type ) const;
virtual void loadFromXmlNode( const pugi::xml_node& node );
virtual void draw();
const std::string& getAlt() const;
UIHTMLImage* setAlt( const std::string& alt );
protected:
UIHTMLImage();
std::string mAlt;
UITextView* mAltLabel{ nullptr };
void createAltLabel();
void removeAltLabel();
};
```
**loadFromXmlNode override:**
```cpp
void UIHTMLImage::loadFromXmlNode( const pugi::xml_node& node ) {
// Read alt attribute before base class processing.
// Note: "alt" is already registered as a tooltip alias in propertydefinition,
// so the base class will handle it as a tooltip. We separately capture mAlt
// for the visual fallback display.
for ( auto& attr : node.attributes() ) {
if ( String::iequals( attr.name(), "alt" ) ) {
mAlt = attr.value();
break;
}
}
beginAttributesTransaction();
UIWidget::loadFromXmlNode( node );
endAttributesTransaction();
// If image failed to load (no drawable) and alt text exists, show alt label
if ( !mDrawable && !mAlt.empty() ) {
createAltLabel();
} else if ( mDrawable && mAltLabel ) {
removeAltLabel();
}
}
```
**alt text fallback:**
- Create a `UITextView` child widget positioned over the image area
- Show it only when `mDrawable` is null and `mAlt` is non-empty
- The text view displays the alt text with appropriate styling (centered, italic, gray)
- Override `draw()` to show either the image or the alt text
- On drawable resource change (image loads later or reloads), remove the alt label
**Registration replacement in UIWidgetCreator:**
```cpp
// Replace this (line 164):
registeredWidget["img"] = [] {
auto img = UIImage::NewWithTag( "img" );
img->setFlags( UI_HTML_ELEMENT );
return img;
};
// With this:
registeredWidget["img"] = [] {
auto img = UIHTMLImage::New();
img->setFlags( UI_HTML_ELEMENT );
return img;
};
```
**Additional note on CSS display:** Since `UIHTMLImage` inherits from `UIImage` (not `UIHTMLWidget`), it inherits the same inline behavior in `rebuildRichText()`. If full CSS `display` support is needed later, consider adding `UIHTMLWidget` to the inheritance chain (or using composition).
---
## Part D: Implementation Order
| Step | Description | Files | Risk |
|---|---|---|---|
| **P1.1** | Add `UI_TYPE_SVG` to `UINodeType` | `uihelper.hpp` | Low |
| **P1.2** | Create `UISvg` header | `uisvg.hpp` (NEW) | Low |
| **P1.3** | Create `UISvg` implementation | `uisvg.cpp` (NEW) | Medium |
| **P1.4** | Register `"svg"` in widget creator | `uiwidgetcreator.cpp` | Low |
| **P1.5** | Regenerate makefiles + compile | premake4 + make | Low |
| **P1.6** | Unit test for inline SVG | `src/tests/unit_tests/` | Medium |
| **P2.x** | Edge cases (no-intrinsic-dims, viewBox, HiDPI, sync fallback) | `uisvg.cpp` | Low-Medium |
| **P3.1** | Create `UIHTMLImage` class | `uihtmlimage.hpp/.cpp` (NEW) | Low |
| **P3.2** | Replace `img` registration | `uiwidgetcreator.cpp` | Low |
| **P3.3** | Unit test for alt text behavior | `src/tests/unit_tests/` | Low |
---
## Part E: Files Summary
### New Files
| File | Purpose |
|---|---|
| `include/eepp/ui/uisvg.hpp` | UISvg class declaration (inherits UIImage) |
| `src/eepp/ui/uisvg.cpp` | UISvg implementation (XML serialization, async SVG rasterization) |
| `include/eepp/ui/uihtmlimage.hpp` | UIHTMLImage class declaration (inherits UIImage, alt text fallback) |
| `src/eepp/ui/uihtmlimage.cpp` | UIHTMLImage implementation |
| `src/tests/unit_tests/htmlsvg.cpp` | Unit tests for inline SVG rendering |
### Modified Files
| File | Change |
|---|---|
| `include/eepp/ui/uihelper.hpp` | Add `UI_TYPE_SVG` to `UINodeType` enum |
| `src/eepp/ui/uiwidgetcreator.cpp` | Register `"svg"` → UISvg; replace `"img"` → UIHTMLImage |
---
## Part F: Potential Hazards
1. **pugi::xml_writer dependency:** The serialization code uses `pugi::xml_writer` which is provided by the included pugixml. No additional dependencies needed.
2. **Pixel vs DP math:** `UIImage::onAutoSize()` and `calcDestSize()` already use `mSize`, `mPaddingPx`, `getPixelsSize()`, and `mDrawable->getPixelsSize()` correctly. The SVG rasterization uses `PixelDensity::getPixelDensity()` for the scale factor. Ensure no dp/pixel confusion in the new code.
3. **Lifetime management — UISvg owns the drawable:**
- `TextureFactory::loadFromMemory()` creates a Texture tracked by the factory with refCount=1
- The texture is wrapped in a `Sprite`, and `Sprite::setAsTextureOwner(true)` makes the Sprite responsible for the Texture's lifetime
- `setDrawable(sprite, true)` makes `UIImage` own the Sprite
- When UISvg is destroyed: `~UIImage()``safeDeleteDrawable()``eeSAFE_DELETE(sprite)``~Sprite()``cleanUpResources()``eeSAFE_DELETE(texture)``~Texture()``TextureFactory::removeReference(this)` → refCount reaches 0 → factory removes entry → GPU texture deleted
- **This follows the exact same pattern as `UIImageViewer::loadImageAsync()`** (`src/eepp/ui/tools/uiimageviewer.cpp:100`)
4. **XML format preservation:** pugi::xml_writer preserves the original formatting. The SVG content is byte-for-byte identical to the serialized XML subtree.
5. **`UI_LOADS_ITS_CHILDREN` side effects:** This flag is also checked by some generic code paths. Verify no negative side effects on layout, clipping, or hit testing.
6. **Thread safety — async rasterization:**
- Rasterization runs on `UISceneNode::getThreadPool()` to avoid blocking the render loop
- `TextureFactory::loadFromMemory()` is called on the thread pool (already proven by `UIImageViewer`)
- The Sprite is constructed on the thread pool (also proven pattern)
- `setDrawable()` and the subsequent `onAutoSize()` / `invalidateDraw()` run on the main thread via `runOnMainThread()`
- Task cancellation: `mTag` tracks the last async task, cleared in destructor via `removeWithTag(this)` to prevent callbacks on destroyed widgets
7. **Large SVG files / Debounce on resize:**
- Re-rasterization is triggered by `onSizeChange()` with a **150ms debounce** via `Node::debounce(cb, delay, uniqueIdentifier)` — if called again before the delay, the previous call is cancelled and the timer resets
- Rasterization is **skipped** if widget has 0 width or height
- Thread pool tag (`removeWithTag(this)`) ensures only the most recent rasterization completes (old tasks are cancelled)
- For huge SVGs, the 150ms debounce prevents multiple expensive parses during rapid layout transitions
8. **Sync fallback:** When no thread pool is available (`!hasThreadPool()`), rasterization happens synchronously on the main thread in `scheduleRasterize()`. This ensures SVGs render in all environments but may cause a frame drop on load.

View File

@@ -129,6 +129,8 @@ enum UINodeType {
UI_TYPE_HTML_HTML,
UI_TYPE_HTML_BODY,
UI_TYPE_HTML_LIST_ITEM,
UI_TYPE_HTML_IMAGE,
UI_TYPE_SVG,
UI_TYPE_MODULES = 10000,
UI_TYPE_TERMINAL = 10001,
UI_TYPE_USER = 200000,

View File

@@ -0,0 +1,34 @@
#ifndef EE_UI_UIHTMLIMAGE_HPP
#define EE_UI_UIHTMLIMAGE_HPP
#include <eepp/ui/uiimage.hpp>
namespace EE { namespace UI {
class EE_API UIHTMLImage : public UIImage {
public:
static UIHTMLImage* New();
virtual ~UIHTMLImage();
virtual Uint32 getType() const;
virtual bool isType( const Uint32& type ) const;
virtual void loadFromXmlNode( const pugi::xml_node& node );
virtual void draw();
const std::string& getAlt() const;
UIHTMLImage* setAlt( const std::string& alt );
protected:
UIHTMLImage();
std::string mAlt;
};
}} // namespace EE::UI
#endif

38
include/eepp/ui/uisvg.hpp Normal file
View File

@@ -0,0 +1,38 @@
#ifndef EE_UI_UISVG_HPP
#define EE_UI_UISVG_HPP
#include <eepp/ui/uiimage.hpp>
namespace EE { namespace UI {
class EE_API UISvg : public UIImage {
public:
static UISvg* New();
virtual ~UISvg();
virtual Uint32 getType() const;
virtual bool isType( const Uint32& type ) const;
virtual void loadFromXmlNode( const pugi::xml_node& node );
const std::string& getSvgXml() const;
protected:
UISvg();
void onSizeChange();
std::string mSvgXml;
Uint64 mTaskId{ 0 };
void loadSvgXml( const pugi::xml_node& node );
void scheduleRasterize();
void rasterizeSvg( const std::string& svgXml );
void clearThreadTag();
};
}} // namespace EE::UI
#endif

View File

@@ -0,0 +1,82 @@
#include <eepp/ui/uihtmlimage.hpp>
#define PUGIXML_HEADER_ONLY
#include <pugixml/pugixml.hpp>
#include <eepp/graphics/pixeldensity.hpp>
#include <eepp/graphics/text.hpp>
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uithememanager.hpp>
namespace EE { namespace UI {
UIHTMLImage* UIHTMLImage::New() {
return eeNew( UIHTMLImage, () );
}
UIHTMLImage::UIHTMLImage() : UIImage( "img" ) {}
UIHTMLImage::~UIHTMLImage() {}
Uint32 UIHTMLImage::getType() const {
return UI_TYPE_HTML_IMAGE;
}
bool UIHTMLImage::isType( const Uint32& type ) const {
return UIHTMLImage::getType() == type ? true : UIImage::isType( type );
}
void UIHTMLImage::loadFromXmlNode( const pugi::xml_node& node ) {
for ( auto& attr : node.attributes() ) {
if ( String::iequals( attr.name(), "alt" ) ) {
mAlt = attr.value();
break;
}
}
beginAttributesTransaction();
UIWidget::loadFromXmlNode( node );
endAttributesTransaction();
}
void UIHTMLImage::draw() {
if ( mVisible && NULL != mDrawable && 0.f != mAlpha ) {
UIImage::draw();
} else if ( mVisible && 0.f != mAlpha && !mAlt.empty() ) {
UINode::draw();
auto* themeManager = getUISceneNode()->getUIThemeManager();
FontStyleConfig fontStyleConfig;
fontStyleConfig.Font = themeManager->getDefaultFont();
fontStyleConfig.CharacterSize = themeManager->getDefaultFontSize();
fontStyleConfig.FontColor = Color( 128, 128, 128, mAlpha );
Float textWidth = Text::getTextWidth( mAlt, fontStyleConfig );
Float availableWidth = mSize.getWidth() - mPaddingPx.Left - mPaddingPx.Right;
Float x = mScreenPos.x + mPaddingPx.Left;
Float y = mScreenPos.y + mPaddingPx.Top;
if ( textWidth < availableWidth )
x += ( availableWidth - textWidth ) / 2;
y += ( mSize.getHeight() - mPaddingPx.Top - mPaddingPx.Bottom -
PixelDensity::getPixelDensity() * fontStyleConfig.CharacterSize ) /
2;
Text::draw( String( mAlt ), Vector2f( x, y ), fontStyleConfig );
}
}
const std::string& UIHTMLImage::getAlt() const {
return mAlt;
}
UIHTMLImage* UIHTMLImage::setAlt( const std::string& alt ) {
if ( mAlt != alt ) {
mAlt = alt;
invalidateDraw();
}
return this;
}
}} // namespace EE::UI

117
src/eepp/ui/uisvg.cpp Normal file
View File

@@ -0,0 +1,117 @@
#include <eepp/ui/uisvg.hpp>
#define PUGIXML_HEADER_ONLY
#include <pugixml/pugixml.hpp>
#include <eepp/graphics/pixeldensity.hpp>
#include <eepp/graphics/sprite.hpp>
#include <eepp/graphics/texturefactory.hpp>
#include <eepp/ui/uiscenenode.hpp>
namespace EE { namespace UI {
namespace {
class XmlStringWriter : public pugi::xml_writer {
public:
std::string result;
virtual void write( const void* data, size_t size ) override {
result.append( static_cast<const char*>( data ), size );
}
};
} // namespace
UISvg* UISvg::New() {
return eeNew( UISvg, () );
}
UISvg::UISvg() : UIImage( "svg" ) {
mFlags |= UI_LOADS_ITS_CHILDREN;
}
UISvg::~UISvg() {
clearThreadTag();
}
Uint32 UISvg::getType() const {
return UI_TYPE_SVG;
}
bool UISvg::isType( const Uint32& type ) const {
return UISvg::getType() == type ? true : UIImage::isType( type );
}
void UISvg::loadFromXmlNode( const pugi::xml_node& node ) {
beginAttributesTransaction();
UIWidget::loadFromXmlNode( node );
endAttributesTransaction();
loadSvgXml( node );
scheduleRasterize();
}
void UISvg::loadSvgXml( const pugi::xml_node& node ) {
XmlStringWriter writer;
node.print( writer );
mSvgXml = writer.result;
}
void UISvg::scheduleRasterize() {
if ( mSvgXml.empty() )
return;
auto size = getPixelsSize();
if ( size.getWidth() <= 0.f || size.getHeight() <= 0.f )
return;
if ( !getUISceneNode()->hasThreadPool() ) {
rasterizeSvg( mSvgXml );
return;
}
clearThreadTag();
std::string svgXml( mSvgXml );
mTaskId = getUISceneNode()->getThreadPool()->run(
[this, svgXml = std::move( svgXml )] { rasterizeSvg( svgXml ); }, {}, (Uint64)this );
}
void UISvg::rasterizeSvg( const std::string& svgXml ) {
Texture* texture = TextureFactory::instance()->loadFromMemory(
(const unsigned char*)svgXml.data(), svgXml.size() );
if ( !texture )
return;
Sprite* sprite = Sprite::New();
sprite->createStatic( texture );
sprite->setAsTextureOwner( true );
sprite->setAsTextureRegionOwner( true );
runOnMainThread( [this, sprite] { setDrawable( sprite, true ); } );
}
void UISvg::onSizeChange() {
UIImage::onSizeChange();
auto size = getPixelsSize();
if ( size.getWidth() <= 0.f || size.getHeight() <= 0.f || mSvgXml.empty() )
return;
debounce( [this] { scheduleRasterize(); }, Milliseconds( 150 ), (UintPtr)this );
}
void UISvg::clearThreadTag() {
if ( mTaskId != 0 && getUISceneNode()->hasThreadPool() ) {
getUISceneNode()->getThreadPool()->removeWithTag( (Uint64)this );
mTaskId = 0;
}
}
const std::string& UISvg::getSvgXml() const {
return mSvgXml;
}
}} // namespace EE::UI

View File

@@ -1837,24 +1837,32 @@ bool UIWidget::applyProperty( const StyleSheetProperty& attribute ) {
notifyLayoutAttrChange();
break;
case PropertyId::Width:
if ( mStyle ) {
mStyle->setStyleSheetProperty(
StyleSheetProperty( "layout-width", attribute.value(), true,
StyleSheetSelectorRule::SpecificityImportant ) );
if ( attribute.value() == "auto" ) {
setLayoutWidthPolicy( SizePolicy::WrapContent );
} else {
if ( mStyle ) {
mStyle->setStyleSheetProperty(
StyleSheetProperty( "layout-width", attribute.value(), true,
StyleSheetSelectorRule::SpecificityImportant ) );
}
setLayoutWidthPolicy( SizePolicy::Fixed );
setSize( eefloor( lengthFromValueAsDp( attribute ) ), getSize().getHeight() );
notifyLayoutAttrChange();
}
setLayoutWidthPolicy( SizePolicy::Fixed );
setSize( eefloor( lengthFromValueAsDp( attribute ) ), getSize().getHeight() );
notifyLayoutAttrChange();
break;
case PropertyId::Height:
if ( mStyle ) {
mStyle->setStyleSheetProperty(
StyleSheetProperty( "layout-height", attribute.value(), true,
StyleSheetSelectorRule::SpecificityImportant ) );
if ( attribute.value() == "auto" ) {
setLayoutHeightPolicy( SizePolicy::WrapContent );
} else {
if ( mStyle ) {
mStyle->setStyleSheetProperty(
StyleSheetProperty( "layout-height", attribute.value(), true,
StyleSheetSelectorRule::SpecificityImportant ) );
}
setLayoutHeightPolicy( SizePolicy::Fixed );
setSize( getSize().getWidth(), eefloor( lengthFromValueAsDp( attribute ) ) );
notifyLayoutAttrChange();
}
setLayoutHeightPolicy( SizePolicy::Fixed );
setSize( getSize().getWidth(), eefloor( lengthFromValueAsDp( attribute ) ) );
notifyLayoutAttrChange();
break;
case PropertyId::BackgroundColor:
setBackgroundColor( attribute.asColor() );

View File

@@ -13,6 +13,8 @@
#include <eepp/ui/uihtmllistitem.hpp>
#include <eepp/ui/uihtmltable.hpp>
#include <eepp/ui/uiimage.hpp>
#include <eepp/ui/uihtmlimage.hpp>
#include <eepp/ui/uisvg.hpp>
#include <eepp/ui/uilinearlayout.hpp>
#include <eepp/ui/uilistbox.hpp>
#include <eepp/ui/uilistview.hpp>
@@ -162,10 +164,15 @@ void UIWidgetCreator::createBaseWidgetList() {
registeredWidget["li"] = UIHTMLListItem::New;
registeredWidget["pre"] = UIRichText::NewPre;
registeredWidget["img"] = [] {
auto img = UIImage::NewWithTag( "img" );
auto img = UIHTMLImage::New();
img->setFlags( UI_HTML_ELEMENT );
return img;
};
registeredWidget["svg"] = [] {
auto svg = UISvg::New();
svg->setFlags( UI_HTML_ELEMENT );
return svg;
};
registeredWidget["input"] = [] { return HTMLInput::New(); };
registeredWidget["header"] = [] { return UIRichText::NewWithTag( "header" ); };
registeredWidget["article"] = [] { return UIRichText::NewWithTag( "article" ); };

View File

@@ -0,0 +1,402 @@
#include "utest.h"
#include <eepp/graphics/fontfamily.hpp>
#include <eepp/graphics/fonttruetype.hpp>
#include <eepp/scene/scenemanager.hpp>
#include <eepp/system/filesystem.hpp>
#include <eepp/system/sys.hpp>
#include <eepp/ui/tools/htmlformatter.hpp>
#include <eepp/ui/uihtmlimage.hpp>
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uisvg.hpp>
#include <eepp/ui/uithememanager.hpp>
#include <eepp/ui/uiwidgetcreator.hpp>
#include <eepp/window/engine.hpp>
using namespace EE;
using namespace EE::Graphics;
using namespace EE::Window;
using namespace EE::Scene;
using namespace EE::UI;
using namespace EE::UI::Tools;
static UI::UISceneNode* createScene() {
Engine::instance()->createWindow( WindowSettings( 800, 600, "SVG Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ) );
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
if ( !font->loaded() ) {
Engine::destroySingleton();
return nullptr;
}
FontFamily::loadFromRegular( font );
UI::UISceneNode* sceneNode = UI::UISceneNode::New();
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
themeManager->setDefaultFont( font );
return sceneNode;
}
static void destroyScene( UI::UISceneNode* sceneNode ) {
eeDelete( sceneNode );
Engine::destroySingleton();
}
static UISvg* findSvgWidget( UIWidget* root ) {
return root->findByType<UISvg>( UI_TYPE_SVG );
}
UTEST( UISvg, basicInlineSvg ) {
auto sceneNode = createScene();
ASSERT_TRUE( sceneNode != nullptr );
std::string html = R"html(<!doctype html>
<html>
<body>
<svg width="100" height="100"><circle cx="50" cy="50" r="40" fill="red"/></svg>
</body>
</html>)html";
auto rootWidget = sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
ASSERT_TRUE( rootWidget != nullptr );
sceneNode->update( Time::Zero );
auto svgWidget = findSvgWidget( rootWidget );
ASSERT_TRUE( svgWidget != nullptr );
EXPECT_TRUE( svgWidget->isType( UI_TYPE_SVG ) );
EXPECT_FALSE( svgWidget->getSvgXml().empty() );
EXPECT_GT( svgWidget->getPixelsSize().getWidth(), 0.f );
EXPECT_GT( svgWidget->getPixelsSize().getHeight(), 0.f );
destroyScene( sceneNode );
}
UTEST( UISvg, svgWithViewBox ) {
auto sceneNode = createScene();
ASSERT_TRUE( sceneNode != nullptr );
std::string html = R"html(<!doctype html>
<html>
<body>
<svg viewBox="0 0 200 200" width="100" height="100"><rect width="100" height="100" fill="blue"/></svg>
</body>
</html>)html";
auto rootWidget = sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
ASSERT_TRUE( rootWidget != nullptr );
sceneNode->update( Time::Zero );
auto svgWidget = findSvgWidget( rootWidget );
ASSERT_TRUE( svgWidget != nullptr );
EXPECT_FALSE( svgWidget->getSvgXml().empty() );
EXPECT_GT( svgWidget->getPixelsSize().getWidth(), 0.f );
EXPECT_GT( svgWidget->getPixelsSize().getHeight(), 0.f );
destroyScene( sceneNode );
}
UTEST( UISvg, svgWithXmlns ) {
auto sceneNode = createScene();
ASSERT_TRUE( sceneNode != nullptr );
std::string html = R"html(<!doctype html>
<html>
<body>
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><rect width="100" height="100" fill="green"/></svg>
</body>
</html>)html";
auto rootWidget = sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
ASSERT_TRUE( rootWidget != nullptr );
sceneNode->update( Time::Zero );
auto svgWidget = findSvgWidget( rootWidget );
ASSERT_TRUE( svgWidget != nullptr );
EXPECT_EQ( svgWidget->getType(), UI_TYPE_SVG );
destroyScene( sceneNode );
}
UTEST( UISvg, svgChildrenNotCreatedAsUiWidgets ) {
auto sceneNode = createScene();
ASSERT_TRUE( sceneNode != nullptr );
std::string html = R"html(<!doctype html>
<html>
<body>
<div>
<svg width="100" height="100">
<circle cx="50" cy="50" r="40" fill="red"/>
<rect width="30" height="30" fill="blue"/>
<path d="M10 10 L20 20" stroke="black"/>
</svg>
</div>
</body>
</html>)html";
auto rootWidget = sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
ASSERT_TRUE( rootWidget != nullptr );
sceneNode->update( Time::Zero );
auto svgWidget = findSvgWidget( rootWidget );
ASSERT_TRUE( svgWidget != nullptr );
EXPECT_TRUE( svgWidget->loadsItsChildren() );
EXPECT_TRUE( svgWidget->isType( UI_TYPE_SVG ) );
// SVG's child count should be 0 because we set UI_LOADS_ITS_CHILDREN
// which prevents the parent from creating widgets for SVG's internal elements
EXPECT_EQ( svgWidget->getChildCount(), 0u );
destroyScene( sceneNode );
}
UTEST( UISvg, multipleSvgElements ) {
auto sceneNode = createScene();
ASSERT_TRUE( sceneNode != nullptr );
std::string html = R"html(<!doctype html>
<html>
<body>
<svg width="50" height="50"><circle cx="25" cy="25" r="20" fill="red"/></svg>
<svg width="50" height="50"><rect width="50" height="50" fill="blue"/></svg>
</body>
</html>)html";
auto rootWidget = sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
ASSERT_TRUE( rootWidget != nullptr );
sceneNode->update( Time::Zero );
auto svgWidgets = rootWidget->findAllByTag( "svg" );
EXPECT_EQ( svgWidgets.size(), 2u );
for ( auto* w : svgWidgets ) {
EXPECT_TRUE( w->isType( UI_TYPE_SVG ) );
EXPECT_FALSE( static_cast<UISvg*>( w )->getSvgXml().empty() );
}
destroyScene( sceneNode );
}
UTEST( UISvg, svgWithNoDimensions ) {
auto sceneNode = createScene();
ASSERT_TRUE( sceneNode != nullptr );
std::string html = R"html(<!doctype html>
<html>
<body>
<svg><circle cx="50" cy="50" r="40" fill="red"/></svg>
</body>
</html>)html";
auto rootWidget = sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
ASSERT_TRUE( rootWidget != nullptr );
sceneNode->update( Time::Zero );
auto svgWidget = findSvgWidget( rootWidget );
ASSERT_TRUE( svgWidget != nullptr );
EXPECT_FALSE( svgWidget->getSvgXml().empty() );
EXPECT_TRUE( svgWidget->isType( UI_TYPE_SVG ) );
destroyScene( sceneNode );
}
UTEST( UISvg, svgInsideBlockElement ) {
auto sceneNode = createScene();
ASSERT_TRUE( sceneNode != nullptr );
std::string html = R"html(<!doctype html>
<html>
<body>
<div>
<p>Before SVG</p>
<svg width="100" height="100"><circle cx="50" cy="50" r="40" fill="red"/></svg>
<p>After SVG</p>
</div>
</body>
</html>)html";
auto rootWidget = sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
ASSERT_TRUE( rootWidget != nullptr );
sceneNode->update( Time::Zero );
auto svgWidget = findSvgWidget( rootWidget );
ASSERT_TRUE( svgWidget != nullptr );
EXPECT_TRUE( svgWidget->isType( UI_TYPE_SVG ) );
EXPECT_FALSE( svgWidget->getSvgXml().empty() );
destroyScene( sceneNode );
}
UTEST( UISvg, svgWithMemoryAsset ) {
auto sceneNode = createScene();
ASSERT_TRUE( sceneNode != nullptr );
std::string html = R"html(<!doctype html>
<html>
<body>
<svg width="100" height="100">
<circle cx="50" cy="50" r="20" fill="red" stroke="black" stroke-width="3"/>
<rect x="30" y="30" width="40" height="40" fill="blue" opacity="0.5"/>
</svg>
</body>
</html>)html";
auto rootWidget = sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
ASSERT_TRUE( rootWidget != nullptr );
sceneNode->update( Time::Zero );
auto svgWidget = findSvgWidget( rootWidget );
ASSERT_TRUE( svgWidget != nullptr );
EXPECT_TRUE( svgWidget->isType( UI_TYPE_SVG ) );
std::string xml = svgWidget->getSvgXml();
EXPECT_FALSE( xml.empty() );
EXPECT_TRUE( xml.find( "<circle" ) != std::string::npos );
EXPECT_TRUE( xml.find( "<rect" ) != std::string::npos );
destroyScene( sceneNode );
}
// --- UIHTMLImage tests ---
UTEST( UIHTMLImage, imgElementCreatesHtmlImage ) {
auto sceneNode = createScene();
ASSERT_TRUE( sceneNode != nullptr );
std::string html = R"html(<!doctype html>
<html>
<body>
<img src="nonexistent.png" alt="Test Image"/>
</body>
</html>)html";
auto rootWidget = sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
ASSERT_TRUE( rootWidget != nullptr );
sceneNode->update( Time::Zero );
auto imgWidget = rootWidget->findByType<UIHTMLImage>( UI_TYPE_HTML_IMAGE );
ASSERT_TRUE( imgWidget != nullptr );
EXPECT_TRUE( imgWidget->isType( UI_TYPE_HTML_IMAGE ) );
EXPECT_STREQ( imgWidget->getElementTag().c_str(), "img" );
destroyScene( sceneNode );
}
UTEST( UIHTMLImage, altAttributeCaptured ) {
auto sceneNode = createScene();
ASSERT_TRUE( sceneNode != nullptr );
std::string html = R"html(<!doctype html>
<html>
<body>
<img src="broken.png" alt="My broken image"/>
</body>
</html>)html";
auto rootWidget = sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
ASSERT_TRUE( rootWidget != nullptr );
sceneNode->update( Time::Zero );
auto imgWidget = rootWidget->findByType<UIHTMLImage>( UI_TYPE_HTML_IMAGE );
ASSERT_TRUE( imgWidget != nullptr );
EXPECT_STREQ( imgWidget->getAlt().c_str(), "My broken image" );
destroyScene( sceneNode );
}
UTEST( UIHTMLImage, noAltAttributeEmpty ) {
auto sceneNode = createScene();
ASSERT_TRUE( sceneNode != nullptr );
std::string html = R"html(<!doctype html>
<html>
<body>
<img src="some.png"/>
</body>
</html>)html";
auto rootWidget = sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
ASSERT_TRUE( rootWidget != nullptr );
sceneNode->update( Time::Zero );
auto imgWidget = rootWidget->findByType<UIHTMLImage>( UI_TYPE_HTML_IMAGE );
ASSERT_TRUE( imgWidget != nullptr );
EXPECT_TRUE( imgWidget->getAlt().empty() );
destroyScene( sceneNode );
}
UTEST( UIHTMLImage, multipleImgElements ) {
auto sceneNode = createScene();
ASSERT_TRUE( sceneNode != nullptr );
std::string html = R"html(<!doctype html>
<html>
<body>
<img alt="First"/>
<img alt="Second"/>
<img alt="Third"/>
</body>
</html>)html";
auto rootWidget = sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
ASSERT_TRUE( rootWidget != nullptr );
sceneNode->update( Time::Zero );
auto imgWidgets = rootWidget->findAllByTag( "img" );
EXPECT_EQ( imgWidgets.size(), 3u );
for ( auto* w : imgWidgets )
EXPECT_TRUE( w->isType( UI_TYPE_HTML_IMAGE ) );
destroyScene( sceneNode );
}
UTEST( UIHTMLImage, imgAndSvgTogether ) {
auto sceneNode = createScene();
ASSERT_TRUE( sceneNode != nullptr );
std::string html = R"html(<!doctype html>
<html>
<body>
<p>Before</p>
<img id="img1" alt="An image"/>
<svg width="50" height="50"><circle cx="25" cy="25" r="20" fill="red"/></svg>
<img id="img2" alt="Another image"/>
</body>
</html>)html";
auto rootWidget = sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
ASSERT_TRUE( rootWidget != nullptr );
sceneNode->update( Time::Zero );
auto svgWidget = findSvgWidget( rootWidget );
ASSERT_TRUE( svgWidget != nullptr );
auto imgWidget1 = rootWidget->find<UIHTMLImage>( "img1" );
ASSERT_TRUE( imgWidget1 != nullptr );
EXPECT_STREQ( imgWidget1->getAlt().c_str(), "An image" );
auto imgWidget2 = rootWidget->find<UIHTMLImage>( "img2" );
ASSERT_TRUE( imgWidget2 != nullptr );
EXPECT_STREQ( imgWidget2->getAlt().c_str(), "Another image" );
destroyScene( sceneNode );
}