Merge branch 'develop' into feature/sdl3

This commit is contained in:
Martín Lucas Golini
2026-05-02 14:45:56 -03:00
159 changed files with 15457 additions and 2257 deletions

View File

@@ -0,0 +1,286 @@
# Border Box Model Content Offset Plan
## Problem Statement
In browser engines, content (text, child widgets) is positioned at `border-width + padding` from the element's edge. In eepp's GUI system, borders are `BorderType::Inside` by default — a pure visual decoration drawn OVER the padding/content area. Content is only offset by padding, ignoring border width entirely. This causes HTML widgets to render text misaligned compared to real browsers.
**User requirement:** UIHTMLWidget elements must behave like browsers — the border should consume space and content should be offset by border + padding.
---
## Key Concepts
| Concept | eepp Current | Browser/CSS | Desired |
|---------|-------------|-------------|---------|
| Border space | 0 (Inside type) | border-width | border-width |
| Content offset | padding only | border + padding | border + padding |
| `box-sizing` | not implemented | `content-box` (default) | add `content-box`/`border-box` |
**BorderType behavior:**
- `Inside` (default): border drawn inside element; `getBorderBoxDiff()` returns zero rect; no space consumed
- `Outside`: border extends outward from element; adds space outside
- `Outline`: border centered on element edge; half inside, half outside
For HTML compatibility, we treat borders as **space-consuming** regardless of BorderType — they push content inward by their width. This matches the CSS `content-box` model where `width` specifies the content area.
---
## Scope: What Changes
### 1. Add helper method to UINode/UIWidget
**File:** `include/eepp/ui/uiwidget.hpp` (declaration), `src/eepp/ui/uiwidget.cpp` (implementation)
Add `getPixelsContentOffset()` that returns a `Rectf` containing `padding + border` for all 4 sides. This becomes the single source of truth for content positioning in HTML widgets.
```cpp
// Returns the content area origin offset = padding + border
Rectf getPixelsContentOffset() const;
```
Implementation:
```cpp
Rectf UIWidget::getPixelsContentOffset() const {
Rectf offset = getPixelsPadding();
if (hasBorder()) {
const auto& b = getBorder()->getBorders();
offset.Left += b.left.width;
offset.Right += b.right.width;
offset.Top += b.top.width;
offset.Bottom += b.bottom.width;
}
return offset;
}
```
**Complexity: LOW** — one new method, ~15 lines.
---
### 2. BlockLayouter — Content Area Calculations
**File:** `src/eepp/ui/blocklayouter.cpp`
All locations that use `mContainer->getPixelsPadding()` for child positioning must switch to `mContainer->getPixelsContentOffset()`.
**Affected lines (~8 sites):**
| Line(s) | Current | Change |
|---------|---------|--------|
| 34-43 `computeIntrinsicWidths` | `getPixelsPadding().Left + .Right` | add border widths to intrinsic size |
| 74-77 `updateLayout` totW | `getPixelsPadding().Left + .Right` | `getPixelsContentOffset().Left + .Right` |
| 88-92 `updateLayout` totH | `getPixelsPadding().Top + .Bottom` | `getPixelsContentOffset().Top + .Bottom` |
| 161-167 `positionRichTextChildren` hitbox | `getPixelsPadding().Left/Top` | `getPixelsContentOffset().Left/Top` |
| 214-216 BR element width | `getPixelsPadding().Left + .Right` | `getPixelsContentOffset().Left + .Right` |
| 227-228 custom widget position | `getPixelsPadding().Left/Top` | `getPixelsContentOffset().Left/Top` |
**Complexity: MEDIUM** — mechanical replacement, ~8 call sites.
---
### 3. UIRichText — Text Rendering & Intrinsic Widths
**File:** `src/eepp/ui/uirichtext.cpp`
| Line(s) | Current | Change |
|---------|---------|--------|
| 180-186 `draw()` | `mScreenPos + mPaddingPx.Left/Top` | add border width to offset |
| 589-590 `rebuildRichText` maxWidth | `- getPixelsPadding().Left - .Right` | `- getPixelsContentOffset().Left - .Right` |
| 638-641 block child width | `- getPixelsPadding().Left - .Right` | `- getPixelsContentOffset().Left - .Right` |
| 665-668 child size computation | same pattern | same |
| 725-728 `getMinIntrinsicWidth` | `+ mPaddingPx.Left + .Right` | `+ getPixelsContentOffset().Left + .Right` |
| 753-756 `getMaxIntrinsicWidth` | same | same |
**Complexity: MEDIUM** — ~6 call sites, same mechanical pattern.
---
### 4. UIHTMLWidget — Out-of-Flow Children
**File:** `src/eepp/ui/uihtmlwidget.cpp`
| Line | Current | Change |
|------|---------|--------|
| 202 `positionOutOfFlowChildren` | `getPixelsPadding().Left, .Top` | `getPixelsContentOffset().Left, .Top` |
Container block origin for absolutely positioned children must include border.
**Complexity: LOW** — single line change.
---
### 5. TableLayouter — Table Layout
**File:** `src/eepp/ui/tablelayouter.cpp`
| Line(s) | Current | Change |
|---------|---------|--------|
| 274-277 `computeIntrinsicWidths` | `getPixelsPadding().Left + .Right` | `getPixelsContentOffset().Left + .Right` |
| 309 available width | `getPixelsPadding().Left + .Right` | `getPixelsContentOffset().Left + .Right` |
| 513, 516 row positioning | `getPixelsPadding().Left` | `getPixelsContentOffset().Left` |
| 527-529 wrap-content height | `getPixelsPadding().Top + .Bottom` | `getPixelsContentOffset().Top + .Bottom` |
**Complexity: LOW** — ~4 call sites.
---
### 6. UIWidget::getMatchParentWidth/Height
**File:** `src/eepp/ui/uiwidget.cpp` (lines ~2577-2617)
These methods calculate how much space a `match_parent` child can use. Currently subtract parent padding; must also subtract parent border.
```cpp
// Before:
Float width = getParent()->getPixelsSize().getWidth() - marginLeft - marginRight -
padding.Left - padding.Right;
// After:
Rectf parentOffset = getParent()->asType<UIWidget>()->getPixelsContentOffset();
Float width = getParent()->getPixelsSize().getWidth() - marginLeft - marginRight -
parentOffset.Left - parentOffset.Right;
```
**Complexity: LOW** — 2 methods, ~4 subtraction lines each.
---
### 7. UIWidget::calculateAutoMargin
**File:** `src/eepp/ui/uiwidget.cpp` (lines ~590-659)
`margin: auto` calculation uses parent padding to determine available space. Must include parent border.
```cpp
// Before:
Float availableWidth = parentSize.getWidth() - parentPadding.Left - parentPadding.Right -
getPixelsSize().getWidth();
// After:
Rectf parentContentOffset = ...getPixelsContentOffset();
Float availableWidth = parentSize.getWidth() - parentContentOffset.Left -
parentContentOffset.Right - getPixelsSize().getWidth();
```
**Complexity: LOW** — ~4 call sites.
---
### 8. (Optional) CSS `box-sizing` Property
**Scope:** Can be deferred. Adding it now would make the fix more complete but doubles the complexity.
If implemented:
- Add `BoxSizing` to `PropertyId` enum (`propertydefinition.hpp`)
- Register property: `registerProperty("box-sizing", "content-box")`
- Under `content-box`: width/height set on content area; border+padding added outside (current plan)
- Under `border-box`: width/height include border+padding; content = width - padding - border (would need reverse calculation)
**Complexity: HIGH** — new property, two calculation modes, affects all width/height resolution. Recommended as follow-up.
---
## Non-Scope / NOT Changing
- **Non-HTML widgets** (UIPushButton, UITextInput, etc.) — they continue using `getPixelsPadding()` directly and border remains decorative.
- **UINode::nodeDraw()` clip regions** — the clipping pipeline already uses `getBorderBoxDiff()` for BorderBox clip; no change needed.
- **UIBorderDrawable rendering** — border geometry generation is unchanged.
- **BorderType behavior** — Inside/Outside/Outline remain as-is; we only USE the border width value for content offset, regardless of type.
- **Background rendering** — backgrounds already render within the padded area; we're only moving content inward.
---
## Test Impact & Validation Protocol
### Expected Test Failures
This change alters the content area origin for all HTML widgets — text, child widgets, intrinsic widths, and match-parent sizing all shift. This means:
**Guaranteed to fail:**
- `UIBorder.renderingVariations` — text inside bordered boxes will shift inward by the border width, changing pixel positions. **This failure IS the expected correct behavior** (the test proves the fix works).
- `UIRichText.anchorMargins` — content offset changes affect the rendered layout.
- `UIRichText.spanPadding` — spans with padding inside bordered containers shift.
- `UIHTMLTable.complexLayout` (1,2,3) — any elements with borders will have their text/content shifted.
**Expected to pass unchanged:**
- Non-HTML widget tests (UILayout, FontRendering, etc.) — these widgets don't use the HTML border model.
- Tests where no element has a border — no content offset change occurs.
**Unknown (may or may not differ):**
- Margin-dependent tests (e.g., `UILayout.marginAuto`) — if parent has a border, `getMatchParentWidth/Height` result changes.
- Layout tests with nested containers — cascading size changes from border inclusion could alter layouts.
### What "Re-generate and Verify" Means
The `compareImages` helper in the unit tests works as follows:
1. **Golden image check:** On each test run, the rendered frame is captured via `win->getFrontBufferImage()` and pixel-compared against a stored `.webp` image at `bin/unit_tests/assets/<folder>/<imageName>.webp`.
2. **Auto-generation on first run:** If the golden image file does not exist, the captured frame is saved AS the new golden image and the test passes. This is how `eepp-ui-border-rendering.webp` was created.
3. **Re-generation for updated rendering:** To update a golden image after an intentional rendering change:
```bash
# Delete the old golden image, re-run the test to auto-create a new one
rm bin/unit_tests/assets/html/eepp-ui-table-complex.webp
ASAN_OPTIONS=detect_leaks=0 xvfb-run -a -s "-screen 0 1280x1024x24" \
bin/unit_tests/eepp-unit_tests-debug --filter="UIHTMLTable.complexLayout"
```
4. **Human validation is REQUIRED after re-generation.** The test will pass automatically once the golden image is regenerated, but this proves nothing — it only proves the rendering is consistent with itself. A human must visually inspect the new rendering (against the old golden image, or against a reference browser rendering) to confirm the change is correct and not a regression. The agent can assist by:
- Describing expected visual differences (e.g., "all text should be shifted right by 4px in bordered elements")
- Comparing pixel dimensions between old and new golden images
- Rendering the same HTML in a reference browser for side-by-side comparison (if the agent has image analysis capabilities)
### Agent Protocol for Failing Tests
When tests fail due to expected rendering changes, the agent MUST:
1. **Report** which tests failed and whether the failure is expected (border-related shift) or unexpected (regression).
2. **Do NOT auto-regenerate** golden images without first describing the expected visual differences to the user.
3. **Request human validation** by explaining what changed and asking the user to confirm the new rendering looks correct. Example: *"The UIBorder.renderingVariations test failed because text inside bordered boxes shifted right by border-left-width and down by border-top-width. I'll regenerate the golden image now — please visually verify the result matches expectations."*
4. **Regenerate golden images only after approval** — delete the old `.webp`, re-run the test, and confirm it passes.
5. **Verify with a reference browser** if the agent has image analysis capabilities — render the same HTML in a browser and compare.
---
## Risk Assessment
| Risk | Severity | Mitigation |
|------|----------|------------|
| Breaking non-HTML widgets | HIGH | Helper method on UIWidget, but only HTML layouters (BlockLayouter, UIRichText, TableLayouter) call it. Non-HTML widgets keep using `getPixelsPadding()` directly. |
| Intrinsic width changes breaking layout | MEDIUM | Run existing HTML layout image tests after each change. Verify pixel-identical rendering with re-generated golden images. |
| Match-parent calculations | MEDIUM | `getMatchParentWidth/Height` is called by ALL widgets, not just HTML. Must gate the border addition on whether parent has a border. |
| Circle dependency on border resolution | LOW | `updateBorders()` is lazy — widths are empty strings until first draw. We must call `mOwner->lengthFromValue(...)` to resolve before reading. In the `getPixelsContentOffset()` method, `getBorder()->getBorders()` accesses already-resolved values — `updateBorders()` is called in `UIBorderDrawable::update()` before draw. |
---
## Implementation Order
1. **Add `getPixelsContentOffset()` method** to UIWidget (declaration + implementation)
2. **Update BlockLayouter** — switch all `getPixelsPadding()` to `getPixelsContentOffset()`
3. **Update UIRichText** — text rendering offset and intrinsic widths
4. **Update UIHTMLWidget** — out-of-flow children offset
5. **Update TableLayouter** — table cell padding offset
6. **Update `getMatchParentWidth/Height`** — gate on parent having border, subtract parent border
7. **Update `calculateAutoMargin`** — gate on parent having border, subtract parent border
8. **Run all tests** — identify which fail and classify as expected vs unexpected
9. **Request human validation** — for all tests with expected failures, describe the visual change and ask for confirmation
10. **Regenerate golden images after approval** — delete old `.webp` files, re-run tests to capture new baseline
11. **Verify non-HTML widgets unaffected** — ensure non-HTML tests still pass with existing golden images
---
## Verification
After implementation, the agent must:
1. **Run the full test suite** and compile a failure report categorizing each as:
- **Expected failure (border shift):** tests where content moved due to border offset — these visually differ from old golden images
- **Unexpected failure:** tests where the change caused a regression — these must be investigated and fixed
- **Passing unchanged:** tests that continue to match their existing golden images
2. **For each expected failure**, describe to the user exactly what changed (e.g., "text in `UIRichText.anchorMargins` shifted right by the container's border-left-width of 4px"). See [Test Impact & Validation Protocol](#test-impact--validation-protocol).
3. **Await human approval** before regenerating any golden images.
4. **After approval**, regenerate golden images for the affected tests and confirm they pass.
5. **Verify** the `UIBorder.renderingVariations` test now produces the correct browser-like rendering (text inside bordered boxes is properly offset by border + padding).

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

@@ -0,0 +1,61 @@
# UI Layout Phase 6: CSS Float & Clear Plan
This document outlines the architectural plan for implementing CSS `float` and `clear` support within the decoupled layout system, leveraging the `Graphics::RichText` engine for mixed content formatting.
**AGENT DIRECTIVE (CRITICAL):** You MUST compile and run the unit tests (`bin/unit_tests/eepp-unit_tests-debug`) after EVERY step. Do NOT proceed to the next step if there is even a 1-pixel difference in visual layout tests. Take a git stash snapshot (`git stash push -m "Phase 6.X passed" && git stash apply`) upon passing a step to keep a checkpoint while continuing to work. **If you need to restore a stash, use `git stash apply` instead of `git stash pop` so the stable snapshot is never lost.**
---
## IMPLEMENTATION HAZARDS (READ BEFORE CODING)
1. **Keyword Collision:** `Float` is a C++ type (`typedef float Float`). When defining the CSS enum, you MUST name it `CSSFloat` to avoid compiler collisions.
2. **Y-Coordinate Interleaving:** `RichText::updateLayout` currently breaks lines independently of their Y position, and only computes Y coordinates *after* all lines are formed. Because floating elements alter the available horizontal width at specific Y coordinate ranges, you will have to calculate `curY` *during* the block iteration, keeping track of active floats to restrict `curX` and `maxWidth`.
3. **Out-Of-Flow Precedence:** Floating elements are *not* out-of-flow in the same way `position: absolute` elements are. `absolute` elements are ignored by layouters, whereas `float` elements strictly participate in and influence the block formatting context (they take up space and push text around). Do not mark them as `isOutOfFlow() = true` in `UIRichText::rebuildRichText`.
---
## Phase 6: Float and Clear implementation
**Step 6.1: CSS Enums and Properties**
- In `csslayouttypes.hpp`, define:
```cpp
enum class CSSFloat { None, Left, Right };
enum class CSSClear { None, Left, Right, Both };
```
And their helper parsing functions (`CSSFloatHelper::fromString`, etc.).
- In `propertydefinition.hpp`, ensure `PropertyId::Float` and `PropertyId::Clear` exist (if not, add them, avoiding conflicts).
- In `UIHTMLWidget`, add `mFloat` and `mClear` members (defaulting to `None`).
- In `UIHTMLWidget::applyProperty`, parse the `Float` and `Clear` properties. Call `notifyLayoutAttrChange()` when they change.
- **Validation:** Compile and run all tests. Must pass. (Snapshot)
**Step 6.2: Extend RichText API**
- In `include/eepp/graphics/richtext.hpp`, update `RichText::addCustomSize`:
```cpp
void addCustomSize( const Sizef& size, bool isBlock, CSSFloat floatType = CSSFloat::None, CSSClear clearType = CSSClear::None );
```
- Update `CustomBlock` struct to store `floatType` and `clearType`.
- In `UIRichText::rebuildRichText`, extract `getCSSFloat()` and `getCSSClear()` from the child widget (defaulting to `None` if the child isn't an HTML widget). Pass these to `richText.addCustomSize`.
- **Validation:** Compile and run all tests. (Snapshot)
**Step 6.3: Core RichText Layout Algorithm (The Tricky Part)**
- In `RichText::updateLayout()`, introduce Y-coordinate awareness during the main loop:
- Create tracking lists: `std::vector<Rectf> leftFloats; std::vector<Rectf> rightFloats;`
- Introduce `Float curY = 0;`
- Before placing *any* block (text or custom), process `clear`: if the block has `clear: left`, advance `curY` past the `bottom` of all `leftFloats`. (Same for `right` and `both`). Reset `curX` and push a new `RenderParagraph` if `curY` changed.
- Compute `availableLeft(curY)` and `availableRight(curY, mMaxWidth)`. Your `curX` must never be less than `availableLeft`.
- **If the block is a float:**
- Place it immediately at `availableLeft` (if left) or `availableRight - width` (if right).
- Add its bounding box `{x, curY, width, height}` to the respective float list.
- Do *not* advance `curX` for the normal inline flow.
- Do *not* trigger a new line for normal flow text (floats are pulled out of the inline line box).
- **If the block is normal text/inline:**
- Adjust `LineWrap::computeLineBreaksEx` to respect the narrowed `mMaxWidth` computed from `availableRight - availableLeft`. *(Note: You may need to handle the case where text wraps below a float and reclaims full width. This can be done by processing text in line-height chunks if constrained by a float).*
- Make sure `curY` is updated when normal lines wrap.
- **Validation:** This is the most complex step. Ensure all existing tests pass exactly (0 pixels difference) before writing float-specific tests. (Snapshot)
**Step 6.4: Float/Clear Layout Tests**
- In `src/tests/unit_tests/uihtml_position_tests.cpp` (or a new `uihtml_float_tests.cpp`), write robust tests for:
- Text wrapping around a `float: left` block.
- Two consecutive `float: left` blocks stacking horizontally.
- A block with `clear: both` jumping below all floats.
- `BlockLayouter` correctly locating the `CustomBlock` widgets where `RichText` positioned the floats.
- **Validation:** Compile and run all tests. Must pass. (Snapshot)

View File

@@ -0,0 +1,55 @@
# UI Layout Phase 8: Form Action and Navigation Plan
This document outlines the architectural plan for implementing HTML `<form>` submissions, input value extraction, and expanding `UISceneNode` to support interceptable navigation requests (GET/POST).
**AGENT DIRECTIVE (CRITICAL):** You MUST compile and run the unit tests (`bin/unit_tests/eepp-unit_tests-debug`) after EVERY step. Do NOT proceed to the next step if there is even a 1-pixel difference in visual layout tests. Take a git stash snapshot (`git stash push -m "Phase 8.X passed" && git stash apply`) upon passing a step to keep a checkpoint while continuing to work. **If you need to restore a stash, use `git stash apply` instead of `git stash pop` so the stable snapshot is never lost.**
---
## Phase 8: Form and Navigation implementation
**Step 8.1: Extend Navigation System**
- The current `UISceneNode::openURL(URI)` and `setURLInterceptorCb` only handle simple URIs, which cannot represent `POST` requests or request bodies.
- In `include/eepp/ui/uiscenenode.hpp`, create:
```cpp
struct NavigationRequest {
URI uri;
std::string method{ "GET" };
std::string body;
std::map<std::string, std::string> extraHeaders;
};
```
- Add `void navigate( const NavigationRequest& request );` to `UISceneNode`.
- Add `void setNavigationInterceptorCb( std::function<bool( const NavigationRequest& request )> cb );`.
- Update `openURL(URI)` to wrap `navigate(NavigationRequest{uri})` for backward compatibility.
- In `navigate()`, if `mNavigationInterceptorCb` returns `true`, return early. Else if `mURLInterceptorCb` returns `true`, return early. Else, fallback to `Engine::instance()->openURI()`.
- **Validation:** Compile and run all tests. (Snapshot)
**Step 8.2: Retrieve Values from Form Elements**
- Form submission requires querying the value of input elements.
- Add `virtual String getValue() const { return String(); }` to `UIWidget`.
- Override `getValue()` in the appropriate classes:
- `HTMLInput`: return `getText()` for text, or `"on"`/`""` for checkboxes/radio buttons based on `isChecked()`.
- `HTMLTextArea` (and `UITextEdit`): return `getText()`.
- `UIDropDownList` (and `UIComboBox`): return the selected item's text.
- Add `virtual String getName() const { return getAttribute("name"); }` or rely on `getAttribute("name")` to get the field identifier.
- **Validation:** Write unit tests to verify `getValue()` for text, checkbox, and dropdowns. Compile and run all tests. (Snapshot)
**Step 8.3: Implement UIHTMLForm**
- Create `UIHTMLForm` class inheriting from `UIHTMLWidget` (or `UIRichText` if treating as a block container).
- Add members: `mAction` (URI), `mMethod` (String, default "GET"), `mEnctype` (String).
- Override `applyProperty` to capture `action`, `method`, and `enctype`.
- Implement `void submit()`.
- `submit()` iterates over all child widgets recursively.
- If a widget has a non-empty `name` attribute (using `getAttribute("name")`), it collects its `getValue()`.
- It URL-encodes the keys and values.
- If `mMethod == "GET"`, it appends the URL-encoded query string to `mAction` and calls `navigate()`.
- If `mMethod == "POST"`, it puts the URL-encoded string into the `body` of `NavigationRequest`, sets `method = "POST"`, and calls `navigate()`.
- In `uiwidgetcreator.cpp`, update `registeredWidget["form"]` to instantiate `UIHTMLForm::New`.
- **Validation:** Compile and run all tests. (Snapshot)
**Step 8.4: Form Submission Triggers & Testing**
- In `UIHTMLForm`, listen for `Event::OnMouseClick` on any child widget. If the target is a submit button (e.g., `HTMLInput` with `type="submit"`, or a `UIPushButton` with `type="submit"`), prevent the default action and call `submit()`.
- Listen to `Event::OnPressEnter` inside text inputs within the form to trigger `submit()`.
- Write a unit test simulating a form with inputs and a submit button. Attach a `NavigationInterceptorCb` to the scene node, simulate a click on the submit button, and verify the intercepted `NavigationRequest` contains the correct URI and encoded body.
- **Validation:** Compile and run all tests. Must pass. (Snapshot)

View File

@@ -0,0 +1,33 @@
# HTML Layout Architecture
This document describes the decoupled HTML/CSS layout engine architecture implemented in `eepp` for `UIHTMLWidget` and related classes.
## Core Concepts
### 1. UIHTMLWidget
`UIHTMLWidget` is the base class for all HTML-like elements. It holds parsed CSS properties (Display, Position, Float, Clear, etc.). Instead of implementing complex layout math directly, it queries a `UILayouterManager` to instantiate the appropriate `UILayouter` based on its `CSSDisplay` property.
### 2. Layouters
Layout math has been extracted from widgets into stateless (or locally stateful) "Layouters":
- **BlockLayouter:** Handles `CSSDisplay::Block`. It positions block-level children vertically. For rich text, it delegates text shaping to the `RichText` engine and simply maps physical coordinates for custom inline widgets.
- **TableLayouter:** Handles `CSSDisplay::Table`. Encapsulates HTML table column width distribution and row positioning.
- **InlineLayouter:** Handles `CSSDisplay::Inline`. *This layouter is empty by design.* Inline formatting (like `<span>` or `<a>`) is completely managed by the nearest Block container (via the `RichText` engine). It acts as a no-op so standard linear layout logic doesn't override text flows.
- **NoneLayouter:** Handles `CSSDisplay::None`. Skips all layout and rendering.
### 3. The UIRichText Engine Integration
`UIRichText` acts as the primary block container for mixed text and widget content.
- It uses `rebuildRichText()` to recursively traverse its children.
- Pure text nodes (`UITextSpan`, `<br>`) are appended to the core `RichText` engine via `RichText::addSpan()`.
- Arbitrary inline widgets (e.g., `<input>`, `<button>`, or images) are passed to the engine via `RichText::addCustomSize()`.
- After `RichText` performs line-wrapping, `BlockLayouter` iterates over the resulting `CustomBlock`s and calls `setPixelsPosition()` on those child widgets to match where the engine placed them.
### 4. Pixel (dp) Math strictly enforced
All layouters **MUST** use Pixel (`Px`) variants of size and padding APIs.
- Use `getPixelsSize()`, `getPixelsPadding()`, and `getLayoutPixelsMargin()`.
- Never use `getSize()` or `getPadding()`, as these return density-independent pixels (dp) and will cause severe calculation bugs on HiDPI displays if mixed with pixel calculations.
### 5. CSS Position (Out-Of-Flow)
Elements with `position: absolute` or `position: fixed`:
- Are ignored by standard Layouters and `UIRichText::rebuildRichText()`.
- Are positioned at the end of the parent's `updateLayout()` using `positionOutOfFlowChildren()`.
- Absolute elements are positioned relative to their `getContainingBlock()` (the nearest positioned ancestor). Fixed elements map to the `UISceneNode` root.

View File

@@ -9,7 +9,7 @@ The test binary manages its own current working directory, so you can execute it
`bin/unit_tests/eepp-unit_tests-debug`
* **Linux & FreeBSD Execution (Required for Desktop Environments):**
Tests open ~50 individual windows. To prevent disrupting the desktop environment, run them in an isolated framebuffer using `xvfb-run`:
`xvfb-run -a -s "-screen 0 1280x1024x24" bin/unit_tests/eepp-unit_tests-debug`
`ASAN_OPTIONS=detect_leaks=0 xvfb-run -a -s "-screen 0 1280x1024x24" bin/unit_tests/eepp-unit_tests-debug`
* **Filtering Tests:**
Use the `--filter` parameter to run specific tests (supports glob patterns).
*Example (runs all tests with "Offset" in the name):*

View File

@@ -36,8 +36,8 @@ jobs:
name: ecode ${{ steps.tag.outputs.version }}
draft: false
prerelease: true
generate_release_notes: true
body: >
generate_release_notes: false
body: |
Builds that include most recent changes as they happen. For stable releases check the whole list of [releases](https://github.com/SpartanJ/ecode/releases).
build_linux_x86_64:
@@ -75,14 +75,34 @@ jobs:
run: |
bash projects/scripts/patch_commit_number.sh
- name: Install dependencies
shell: bash
run: |
apt-get install -y curl libfuse2 fuse premake4 mesa-common-dev libgl1-mesa-dev sudo file appstream
curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
apt-get install -y nodejs
add-apt-repository -y universe
add-apt-repository -y multiverse
add-apt-repository -y ppa:ubuntu-toolchain-r/test
apt-get update
SUCCESS=false
for i in {1..3}; do
echo "Attempt $i: Adding PPA via Launchpad API..."
if timeout 30s sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test; then
SUCCESS=true
echo "Successfully added PPA."
break
fi
echo "Launchpad API failed or timed out. Retrying in 5 seconds..."
sleep 5
done
if [ "$SUCCESS" = false ]; then
echo "add-apt-repository failed 3 times. Executing manual fallback..."
. /etc/os-release
echo "deb http://ppa.launchpad.net/ubuntu-toolchain-r/test/ubuntu $VERSION_CODENAME main" | sudo tee /etc/apt/sources.list.d/ubuntu-toolchain-r-fallback.list
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 60C317803A41BA51845E371A1E9377A2BA9EF27F || true
fi
sudo apt-get update
apt-get install -y gcc-13 g++-13 libdw-dev
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 10
update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 10
@@ -407,7 +427,7 @@ jobs:
disable-cache: true
prepare: |
pkg upgrade -y
pkg install -y bash git sdl2 curl premake5 gsed gmake
pkg install -y bash pcre2 git sdl2 curl premake5 gsed gmake
run: |
export CC=clang
export CXX=clang++

View File

@@ -99,7 +99,6 @@ Diese Operation kann nicht rückgängig gemacht werden!</string>
<string name="chat_history">Unterhaltungsverlauf</string>
<string name="check_for_new_updates_at_startup">Bei Programmstart nach Aktualisierungen suchen</string>
<string name="check_for_updates">Prüfe auf Aktualisierungen</string>
<string name="check_for_updates_ellipsis">Prüfe auf Aktualisierungen...</string>
<string name="check_languages_health">Sprachenintegrität prüfen</string>
<string name="clean">Bereinigen</string>
<string name="clean_failed">In der Bereinigung traten Fehler auf
@@ -171,7 +170,6 @@ Alle ungespeicherten Änderungen gehen dann verloren.</string>
<string name="continue_editing">Bearbeitung fortsetzen</string>
<string name="copy">Kopieren</string>
<string name="copy_containing_folder_path">Ordnerpfad kopieren</string>
<string name="copy_containing_folder_path_ellipsis">Ordnerpfad kopieren...</string>
<string name="copy_error_message">Fehlermeldung kopieren</string>
<string name="copy_file_path">Dateipfad kopieren</string>
<string name="copy_file_path_and_position">Dateipfad und -position kopieren</string>
@@ -260,7 +258,6 @@ Dateipfad: </string>
<string name="duplicate_file_ellipsis">Datei duplizieren...</string>
<string name="ecode_no_updates_available">Gegenwärtig steht keine Aktualisierung an.</string>
<string name="ecode_source">ecode-Quelltext</string>
<string name="ecode_source_ellipsis">ecode-Quelltext...</string>
<string name="ecode_unreleased_version">Sie führen eine unveröffentlichte Version von ecode aus.
Gegenwärtige Version: </string>
<string name="ecode_updates_available"> verfügbar!
@@ -412,7 +409,7 @@ Für sichtbare Änderung ecode neu starten.</string>
<string name="go_to_line_in_current_document">In aktuellem Dokument zu Zeile springen</string>
<string name="gone">verschwunden</string> <!-- ###STATE### -->
<string name="group">Gruppierung</string>
<string name="treat_h_files_as_ellipsis">.h-Dateien als C++-Code ansehen.</string>
<string name="treat_h_files_as">.h-Dateien als ansehen</string>
<string name="help">Hilfe</string>
<string name="hide">Ausblenden</string>
<string name="hide_tabbar_on_single_tab">Tableiste bei einzelnem Tab ausblenden</string>
@@ -503,10 +500,8 @@ Beachten Sie, dass der Großteil der Tastenzuweisungen hier definiert wird.</str
<string name="more_ellipsis">Mehr...</string>
<string name="more_options">Weitere Optionen</string>
<string name="move_down">Nach unten</string>
<string name="move_panel_left">Paneel nach links bewegen</string>
<string name="move_panel_left_ellipsis">Paneel nach links bewegen...</string>
<string name="move_panel_right">Paneel nach rechts bewegen</string>
<string name="move_panel_right_ellipsis">Paneel nach rechts bewegen...</string>
<string name="move_panel_to_left">Paneel nach links bewegen</string>
<string name="move_panel_to_right">Paneel nach rechts bewegen</string>
<string name="move_scroll_down">Scrollen nach unten bewegen</string>
<string name="move_scroll_up">Scrollen nach oben bewegen</string>
<string name="move_to_end_of_line">Zum Zeilenende bewegen</string>
@@ -868,7 +863,7 @@ in der Baumansicht und in Öffen-/Schließdialogen aktivieren.</string>
<string name="ui_scale_factor">Oberflächenskalierung</string>
<string name="ui_thene">Thema der Oberfläche</string>
<string name="uicodeeditor_copy">Kopieren</string>
<string name="uicodeeditor_copy_containing_folder_path_ellipsis">Überordnerpfad kopieren...</string>
<string name="uicodeeditor_copy_containing_folder_path">Überordnerpfad kopieren</string>
<string name="uicodeeditor_copy_file_path">Dateipfad kopieren</string>
<string name="uicodeeditor_copy_file_path_and_position">Dateipfad und -position kopieren</string>
<string name="uicodeeditor_cut">Ausschneiden</string>
@@ -926,7 +921,7 @@ Für sichtbare Änderung ecode neu starten.</string>
<string name="workspace_symbol_find_ellipsis">Arbeitsbereichssymbol suchen...</string>
<string name="wrap_letter">Zeichentrennung</string>
<string name="wrap_mode">Trennmodus</string>
<string name="wrap_type_ellipsis">Trennen an...</string>
<string name="wrap_type">Trennen an</string>
<string name="wrap_word">Worttrennung</string>
<string name="write_unicode_bom">Unicode-BOM schreiben</string>
<string name="you_have_not_yet_opened_a_folder">Es wurde noch kein Ordner geöffnet.</string>

View File

@@ -83,7 +83,6 @@ This operation cannot be reverted!</string>
<string name="chat_history">Chat History</string>
<string name="check_for_new_updates_at_startup">Always check for new updates at startup.</string>
<string name="check_for_updates">Check For Updates</string>
<string name="check_for_updates_ellipsis">Check for Updates...</string>
<string name="check_languages_health">Check Languages Health</string>
<string name="clean">Clean</string>
<string name="clean_failed">Clean run with errors
@@ -155,7 +154,6 @@ All changes will be lost.</string>
<string name="continue_editing">Continue Editing</string>
<string name="copy">Copy</string>
<string name="copy_containing_folder_path">Copy Containing Folder Path</string>
<string name="copy_containing_folder_path_ellipsis">Copy Containing Folder Path...</string>
<string name="copy_error_message">Copy Error Message</string>
<string name="copy_file_path">Copy File Path</string>
<string name="copy_file_path_and_position">Copy File Path And Position</string>
@@ -243,8 +241,7 @@ File path is: </string>
<string name="duplicate_file">Duplicate file</string>
<string name="duplicate_file_ellipsis">Duplicate File...</string>
<string name="ecode_no_updates_available">There are currently no updates available.</string>
<string name="ecode_source">Ecode Source</string>
<string name="ecode_source_ellipsis">ecode source code...</string>
<string name="ecode_source">ecode Source Code</string>
<string name="ecode_unreleased_version">You are running an unreleased version of ecode!
Current version: </string>
<string name="ecode_updates_available"> is available!
@@ -396,7 +393,7 @@ Restart ecode to see the changes.</string>
<string name="go_to_line_in_current_document">Go To Line in Current Document</string>
<string name="gone">gone</string>
<string name="group">Group</string>
<string name="treat_h_files_as_ellipsis">Treat .h files as C++ code.</string>
<string name="treat_h_files_as">Treat .h files as</string>
<string name="help">Help</string>
<string name="hide">Hide</string>
<string name="hide_tabbar_on_single_tab">Hide tabbar on single tab</string>
@@ -487,10 +484,8 @@ Be aware that many of the core keybindings can be found there.</string>
<string name="more_ellipsis">More...</string>
<string name="more_options">More Options</string>
<string name="move_down">Move Down</string>
<string name="move_panel_left">Move Panel Left</string>
<string name="move_panel_left_ellipsis">Move panel to left...</string>
<string name="move_panel_right">Move Panel Right</string>
<string name="move_panel_right_ellipsis">Move panel to right...</string>
<string name="move_panel_to_left">Move Panel To Left</string>
<string name="move_panel_to_right">Move Panel To Right</string>
<string name="move_scroll_down">Move Scroll Down</string>
<string name="move_scroll_up">Move Scroll Up</string>
<string name="move_to_end_of_line">Move To End Of Line</string>
@@ -623,7 +618,7 @@ Please check that the application directory has write permissions.</string>
<string name="recent_folders_ellipsis">Recent Folders...</string>
<string name="redo">Redo</string>
<string name="refresh_model_ui">Refresh Local Models</string>
<string name="refresh_view_ellipsis">Refresh View...</string>
<string name="refresh_view">Refresh View</string>
<string name="regex_match">Regular Expression Match</string>
<string name="regular_expression">Regular Expression</string>
<string name="reload">Reload</string>
@@ -852,7 +847,7 @@ the directory tree and in file dialogs to open a folder or file.</string>
<string name="ui_scale_factor">Ui Scale Factor</string>
<string name="ui_thene">UI Theme</string>
<string name="uicodeeditor_copy">Copy</string>
<string name="uicodeeditor_copy_containing_folder_path_ellipsis">Copy Containing Folder Path...</string>
<string name="uicodeeditor_copy_containing_folder_path">Copy Containing Folder Path</string>
<string name="uicodeeditor_copy_file_path">Copy File Path</string>
<string name="uicodeeditor_copy_file_path_and_position">Copy File Path and Position</string>
<string name="uicodeeditor_cut">Cut</string>
@@ -910,7 +905,7 @@ Restart ecode to see the changes.</string>
<string name="workspace_symbol_find_ellipsis">Search Workspace Symbol...</string>
<string name="wrap_letter">Letter wrap</string>
<string name="wrap_mode">Wrap Mode</string>
<string name="wrap_type_ellipsis">Wrap Against...</string>
<string name="wrap_type">Wrap Against</string>
<string name="wrap_word">Word wrap</string>
<string name="write_unicode_bom">Write Unicode BOM</string>
<string name="you_have_not_yet_opened_a_folder">You have not yet opened a folder.</string>

View File

@@ -84,7 +84,6 @@ Cette opération est irréversible !</string>
<string name="chat_history">Historique des discussions</string>
<string name="check_for_new_updates_at_startup">Toujours vérifier les nouvelles mises à jour au démarrage.</string>
<string name="check_for_updates">Rechercher les mises à jour</string>
<string name="check_for_updates_ellipsis">Rechercher les mises à jour...</string>
<string name="check_languages_health">Vérifier l'état des langues</string>
<string name="clean">Nettoyer</string>
<string name="clean_failed">Le nettoyage s'est exécuté avec des erreurs
@@ -156,7 +155,6 @@ Toutes les modifications seront perdues.</string>
<string name="continue_editing">Continuer la modification</string>
<string name="copy">Copier</string>
<string name="copy_containing_folder_path">Copier le chemin du dossier parent</string>
<string name="copy_containing_folder_path_ellipsis">Copier le chemin du dossier parent...</string>
<string name="copy_error_message">Copier le message d'erreur</string>
<string name="copy_file_path">Copier le chemin du fichier</string>
<string name="copy_file_path_and_position">Copier le chemin et la position du fichier</string>
@@ -245,7 +243,6 @@ Le chemin du fichier est : </string>
<string name="duplicate_file_ellipsis">Dupliquer le fichier...</string>
<string name="ecode_no_updates_available">Il n'y a actuellement aucune mise à jour disponible.</string>
<string name="ecode_source">Code source de ecode</string>
<string name="ecode_source_ellipsis">Code source de ecode...</string>
<string name="ecode_unreleased_version">Vous utilisez une version non publiée d'ecode !
Version actuelle : </string>
<string name="ecode_updates_available"> est disponible !
@@ -395,7 +392,7 @@ Redémarrez ecode pour voir les changements.</string>
<string name="go_to_line_in_current_document">Aller à la ligne dans le document actuel</string>
<string name="gone">supprimé</string>
<string name="group">Groupe</string>
<string name="treat_h_files_as_ellipsis">Traiter les fichiers .h comme du code C++.</string>
<string name="treat_h_files_as">Traiter les fichiers .h comme</string>
<string name="help">Aide</string>
<string name="hide">Masquer</string>
<string name="hide_tabbar_on_single_tab">Masquer la barre d'onglets sur un seul onglet</string>
@@ -410,7 +407,7 @@ Redémarrez ecode pour voir les changements.</string>
<string name="indent">Indenter</string>
<string name="indent_tab_alignment">Alignement de l'indentation</string>
<string name="indent_tab_character">Caractère à utiliser pour l'indentation</string>
<string name="indent_width">Largeur d'indentation</string>
<string name="indent_width">Taille d'indentation</string>
<string name="indentation_type">Type d'indentation</string>
<string name="inode">Nœud-i</string>
<string name="insert_search_query">Insérer la requête de recherche</string>
@@ -486,10 +483,8 @@ La mémoire de discussion ne pourra être supprimée manuellement que dans l'his
<string name="more_ellipsis">Plus...</string>
<string name="more_options">Plus d'options</string>
<string name="move_down">Déplacer vers le bas</string>
<string name="move_panel_left">Déplacer le panneau vers la gauche</string>
<string name="move_panel_left_ellipsis">Déplacer le panneau vers la gauche...</string>
<string name="move_panel_right">Déplacer le panneau vers la droite</string>
<string name="move_panel_right_ellipsis">Déplacer le panneau vers la droite...</string>
<string name="move_panel_to_left">Déplacer le panneau vers la gauche</string>
<string name="move_panel_to_right">Déplacer le panneau vers la droite</string>
<string name="move_scroll_down">Déplacer le défilement vers le bas</string>
<string name="move_scroll_up">Déplacer le défilement vers le haut</string>
<string name="move_to_end_of_line">Aller à la fin de la ligne</string>
@@ -780,8 +775,8 @@ La valeur minimale est 1 et le maximum est 6. Nécessite un redémarrage.</strin
dans l'arborescence du répertoire.</string>
<string name="syntax_color_scheme">Schéma de couleurs syntaxique</string>
<string name="system">Système</string>
<string name="tab_width">Largeur des onglets</string>
<string name="tabs">Onglets</string>
<string name="tab_width">Taille des tabulations</string>
<string name="tabs">Tabulations</string>
<string name="terminal">Terminal</string>
<string name="terminal_color_scheme">Schéma de couleurs du terminal</string>
<string name="terminal_color_scheme_set">Schéma de couleurs du terminal : %s</string>
@@ -843,7 +838,7 @@ dans l'arborescence de répertoires ainsi que dans les boites de dialogues de s
<string name="ui_scale_factor">Interface utilisateur : facteur d'échelle</string>
<string name="ui_thene">Interface utilisateur : thème</string>
<string name="uicodeeditor_copy">Copier</string>
<string name="uicodeeditor_copy_containing_folder_path_ellipsis">Copier le chemin du dossier parent...</string>
<string name="uicodeeditor_copy_containing_folder_path">Copier le chemin du dossier parent</string>
<string name="uicodeeditor_copy_file_path">Copier le chemin du fichier</string>
<string name="uicodeeditor_copy_file_path_and_position">Copier le chemin du fichier et sa position</string>
<string name="uicodeeditor_cut">Couper</string>
@@ -886,7 +881,7 @@ dans l'arborescence de répertoires ainsi que dans les boites de dialogues de s
<string name="variable_name">Nom de la variable</string>
<string name="variables">Variables</string>
<string name="view">Vue</string>
<string name="viewport">Viewport</string>
<string name="viewport">Fenêtre d'affichage</string>
<string name="vsync">VSync</string>
<string name="vsync_changed">Vsync : configuration mise à jour.
Redémarrer ecode pour voir les changements.</string>
@@ -899,10 +894,10 @@ Redémarrer ecode pour voir les changements.</string>
<string name="working_dir_at">Répertoire de travail %s
</string>
<string name="workspace_symbol_find_ellipsis">Rechercher les symboles dans l'espace de travail...</string>
<string name="wrap_letter">Letter wrap</string>
<string name="wrap_mode">Wrap Mode</string>
<string name="wrap_type_ellipsis">Wrap Against...</string>
<string name="wrap_word">Word wrap</string>
<string name="wrap_letter">Au caractère</string>
<string name="wrap_mode">Mode de retour à la ligne</string>
<string name="wrap_type">Retour à la ligne</string>
<string name="wrap_word">Au mot</string>
<string name="write_unicode_bom">Écrire le BOM Unicode</string>
<string name="you_have_not_yet_opened_a_folder">Vous n'avez pas encore ouvert de dossier.</string>
<string name="zoom_in">Zoomer</string>
@@ -913,5 +908,3 @@ Redémarrer ecode pour voir les changements.</string>
<string name="terminal_paste">Coller dans le terminal</string>
<string name="terminal_rename">Renommer le terminal</string>
</resources>
me="terminal_rename">Renommer le terminal</string>
</resources>

View File

@@ -123,7 +123,6 @@
<string name="console_toggle">Console Toggle</string>
<string name="copy">复制</string>
<string name="copy_containing_folder_path">Copy Containing Folder Path</string>
<string name="copy_containing_folder_path_ellipsis">Copy Containing Folder Path...</string>
<string name="copy_error_message">复制错误信息</string>
<string name="copy_file_path">复制文件路径</string>
<string name="copy_file_path_and_position">复制文件路径</string>
@@ -377,10 +376,8 @@ Name your stash (optional):</string>
<string name="monospace_font_ellipsis">Monospace Font...</string>
<string name="more_ellipsis">更多...</string>
<string name="move_down">下移</string>
<string name="move_panel_left">面板左移</string>
<string name="move_panel_left_ellipsis">面板左移...</string>
<string name="move_panel_right">面板右移</string>
<string name="move_panel_right_ellipsis">面板右移...</string>
<string name="move_panel_to_left">面板左移</string>
<string name="move_panel_to_right">面板右移</string>
<string name="move_scroll_down">下一页</string>
<string name="move_scroll_up">上一页</string>
<string name="move_to_next_line">下一行</string>
@@ -639,7 +636,7 @@ file in the directory tree.</string>
<string name="ui_scale_factor">界面缩放比例</string>
<string name="ui_thene">界面主题</string>
<string name="uicodeeditor_copy">复制</string>
<string name="uicodeeditor_copy_containing_folder_path_ellipsis">复制所在文件夹路径...</string>
<string name="uicodeeditor_copy_containing_folder_path">复制所在文件夹路径</string>
<string name="uicodeeditor_copy_file_path">复制文件路径</string>
<string name="uicodeeditor_copy_file_path_and_position">复制文件路径和位置</string>
<string name="uicodeeditor_cut">剪切</string>

View File

@@ -9,74 +9,36 @@
"name": "claude-opus-4-6"
},
{
"cache_configuration": {
"max_cache_anchors": 4,
"min_total_token": 2048,
"should_speculate": true
},
"default_temperature": 1.0,
"display_name": "Claude Opus 4.7",
"name": "claude-opus-4-7"
},
{
"display_name": "Claude Opus 4.5",
"max_output_tokens": 8192,
"max_tokens": 200000,
"name": "claude-opus-4-5-20251101"
},
{
"cache_configuration": {
"max_cache_anchors": 4,
"min_total_token": 2048,
"should_speculate": true
},
"default_temperature": 1.0,
"display_name": "Claude Haiku 4.5",
"max_output_tokens": 8192,
"max_tokens": 200000,
"name": "claude-haiku-4-5-20251001"
},
{
"cache_configuration": {
"max_cache_anchors": 4,
"min_total_token": 2048,
"should_speculate": true
},
"default_temperature": 1.0,
"display_name": "Claude Sonnet 4.5",
"max_output_tokens": 8192,
"max_tokens": 200000,
"name": "claude-sonnet-4-5-20250929"
},
{
"cache_configuration": {
"max_cache_anchors": 4,
"min_total_token": 2048,
"should_speculate": true
},
"default_temperature": 1.0,
"display_name": "Claude Opus 4.1",
"max_output_tokens": 8192,
"max_tokens": 200000,
"name": "claude-opus-4-1-20250805"
},
{
"cache_configuration": {
"max_cache_anchors": 4,
"min_total_token": 2048,
"should_speculate": true
},
"default_temperature": 1.0,
"display_name": "Claude Opus 4",
"max_output_tokens": 8192,
"max_tokens": 200000,
"name": "claude-opus-4-20250514"
},
{
"cache_configuration": {
"max_cache_anchors": 4,
"min_total_token": 2048,
"should_speculate": true
},
"default_temperature": 1.0,
"display_name": "Claude Sonnet 4",
"max_output_tokens": 8192,
"max_tokens": 200000,
"name": "claude-sonnet-4-20250514"
}
@@ -88,17 +50,13 @@
"display_name": "DeepSeek",
"models": [
{
"display_name": "DeepSeek Chat",
"max_output_tokens": 8192,
"max_tokens": 64000,
"name": "deepseek-chat",
"display_name": "DeepSeek V4 Flash",
"name": "deepseek-v4-flash",
"cheapest": true
},
{
"display_name": "DeepSeek Reasoner",
"max_output_tokens": 8192,
"max_tokens": 64000,
"name": "deepseek-reasoner"
"display_name": "DeepSeek V4 Pro",
"name": "deepseek-v4-pro"
}
],
"version": 1
@@ -246,6 +204,18 @@
{
"name": "gpt-5.2"
},
{
"name": "gpt-5.3"
},
{
"name": "gpt-5.4"
},
{
"name": "gpt-5.5"
},
{
"name": "gpt-5.5-pro"
},
{
"name": "gpt-5.2-pro"
},
@@ -486,6 +456,9 @@
"models": [
{
"name": "kimi-k2.5"
},
{
"name": "kimi-k2.6"
}
]
},
@@ -524,6 +497,10 @@
"api_url": "https://api.together.xyz/v1/chat/completions",
"display_name": "Together AI",
"models": [
{
"name": "deepseek-ai/DeepSeek-V4-Pro",
"display_name": "Deepseek V4 Pro"
},
{
"name": "zai-org/GLM-5.1",
"display_name": "GLM 5.1 FP4"
@@ -532,6 +509,10 @@
"name": "moonshotai/Kimi-K2.5",
"display_name": "Kimi K2.5"
},
{
"name": "moonshotai/Kimi-K2.6",
"display_name": "Kimi K2.6 Fp4"
},
{
"name": "MiniMaxAI/MiniMax-M2.7",
"display_name": "MiniMax M2.7 FP4"

View File

@@ -62,6 +62,10 @@ strong {
font-style: bold;
}
small {
font-size: smaller;
}
u,
ins {
text-decoration: underline;
@@ -77,39 +81,38 @@ em {
font-style: italic;
}
CodeEditor,
code {
font-family: monospace;
}
h1 {
font-size: 2em;
margin: 0.67em 0;
font-weight: bold;
}
h2 {
font-size: 1.5em;
margin: 0.83em 0;
font-weight: bold;
}
h3 {
font-size: 1.17em;
margin: 1em 0;
font-weight: bold;
}
h4 {
font-size: 1em;
margin: 1.33em 0;
font-weight: bold;
}
h5 {
font-size: 0.83em;
margin: 1.67em 0;
font-weight: bold;
}
h6 {
font-size: 0.67em;
margin: 2.33em 0;
font-weight: bold;
}
@@ -120,76 +123,18 @@ table, td {
hr {
min-height: 1dp;
background-color: gray;
margin: 0.5em 0;
}
center {
text-align: center;
}
p, ol, ul, pre, blockquote {
margin: 1em 0;
ul {
list-style-type: disc;
}
li > p {
margin: 0;
}
ol, ul {
margin: 0.67em 0;
}
ul > li,
ol > li {
padding-left: 2em;
}
ol > li {
background-tint: var(--font);
background-position: 0.6em 0.3em;
}
ol > li:nth-child(1) {
background-image: glyph("monospace", 1em, "1");
}
ol > li:nth-child(2) {
background-image: glyph("monospace", 1em, "2");
}
ol > li:nth-child(3) {
background-image: glyph("monospace", 1em, "3");
}
ol > li:nth-child(4) {
background-image: glyph("monospace", 1em, "4");
}
ol > li:nth-child(5) {
background-image: glyph("monospace", 1em, "5");
}
ol > li:nth-child(6) {
background-image: glyph("monospace", 1em, "6");
}
ol > li:nth-child(7) {
background-image: glyph("monospace", 1em, "7");
}
ol > li:nth-child(8) {
background-image: glyph("monospace", 1em, "8");
}
ol > li:nth-child(9) {
background-image: glyph("monospace", 1em, "9");
}
ul > li {
background-image: url("data:image/svg,<svg viewBox='0 0 24 24' width='12' height='12' fill='#ffffff'><path d='M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z'></path></svg>");
background-tint: var(--font);
background-position: 0.6em 0.45em;
background-size: 0.5em 0.5em;
ol {
list-style-type: decimal;
}
a {
@@ -218,14 +163,6 @@ blockquote {
border-left: 2dp solid var(--tab-line);
}
blockquote > *:first-child {
margin-top: 0dp;
}
blockquote > *:last-child {
margin-bottom: 0dp;
}
MarkdownView p,
MarkdownView ol,
MarkdownView ul,
@@ -234,6 +171,19 @@ MarkdownView blockquote {
margin-top: 0;
}
MarkdownView ul,
MarkdownView ol {
padding-left: 21dp;
}
MarkdownView ul ul,
MarkdownView ul ol,
MarkdownView ol ul,
MarkdownView ol ol {
margin-top: 0;
margin-bottom: 0;
}
MarkdownView a {
color: var(--primary);
selection-color: var(--font-selected-pressed);
@@ -250,6 +200,10 @@ MarkdownView {
padding: 4dp;
}
MarkdownView li > p { margin: 0; }
MarkdownView blockquote > *:first-child { margin-top: 0dp; }
MarkdownView blockquote > *:last-child { margin-bottom: 0dp; }
MarkdownView h1,
MarkdownView h2 {
border-bottom: 1dp solid var(--tab-line);
@@ -265,13 +219,12 @@ MarkdownView table > thead > tr > th {
font-style: bold;
}
MarkdownView CodeEditor {
padding: 4dp;
}
MarkdownView CodeEditor,
MarkdownView code {
font-family: monospace;
background-color: var(--button-back);
font-size: 11dp;
padding: 4dp;
}
pushbutton,
@@ -838,7 +791,8 @@ Loader {
fill-color: var(--primary);
}
CodeEditor > Loader {
CodeEditor > Loader,
code > Loader {
background-color: #0000002d;
radius: 32dp;
outline-thickness: 6dp;

View File

@@ -0,0 +1,54 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<style type="text/css">
body {
background: #000000;
color: #FFFFFF;
font-family: Verdana, Helvetica, Arial, sans-serif;
font-size: 8pt;
font-weight:normal;
text-align: left;
margin: 0;
padding: 0;
}
#main {
width: 400px;
position:absolute;
left:50%;
margin-left: -200px;
margin-top: 120px;
border: 0px;
}
.box {
width: 400px;
margin: 0 auto;
background-color: #333333;
text-align: center;
}
</style>
</head>
<body>
<div id="main">
<div class="box">
<div class="titlebox">File Upload</div>
<div class="login_inbox">
<div class="loginbox">
<div class="mini_titlebox">[LOGIN]</div>
<form method="post" action="?s=1">
<p>Username: <input name="Nombre" type="text" size="12" /></p>
<p>Password: <input name="Password" type="password" size="12" /></p>
<p><input type="submit" name="Submit" value="Entrar" /></p>
</form>
</div>
</div>
</div>
<div class="box">
<div class="titlebox">Download Files</div>
<div class="inbox">
<a href="upload/">ENTER</a>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,65 @@
<!doctype html>
<html lang="en">
<head>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #1e2a38;
font:
13px Verdana,
Sans-Serif;
color: #d0d8e0;
line-height: 1.5;
}
a {
color: #60c0e0;
text-decoration: underline;
}
a:hover {
background-color: #2a3a4a;
color: #80d8f0;
}
nav {
background-color: #162028;
text-align: center;
padding: 8px;
position: sticky;
top: 0;
z-index: 10;
border-bottom: 1px solid #2a3a4a;
}
nav a {
margin: 0 10px;
font:
bold 11px Verdana,
Sans-Serif;
text-decoration: none;
color: #80b8d0;
}
nav a:hover {
background-color: #2a3a4a;
color: #a0d8f0;
}
</style>
</head>
<body>
<nav>
<a href="#home">Home</a>
<a href="#features">Features</a>
<a href="#super-enhancement">Super Enhancement</a>
<a href="#downloads">Downloads</a>
<a href="#coming">Coming Soon</a>
<a href="#legal">Legal</a>
</nav>
</body>
</html>

View File

@@ -0,0 +1,128 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background-color: #f5f5f5;
font: 11px Verdana, Sans-Serif;
color: #333;
padding: 8px;
}
.row { clear: both; margin-bottom: 4px; }
.label {
float: left;
width: 130px;
font-size: 9px;
line-height: 1.2;
color: #555;
padding-top: 2px;
padding-right: 4px;
text-align: right;
}
.box {
float: left;
width: 48px;
height: 48px;
margin: 2px;
background-color: #e8f0f8;
font-size: 8px;
text-align: center;
padding: 4px;
overflow: hidden;
}
.clearfix { clear: both; }
</style>
</head>
<body>
<!-- ============ 1 BORDER ============ -->
<div class="row">
<div class="label">1 border</div>
<div class="box" style="border-top: 4px solid #e74c3c;">top</div>
<div class="box" style="border-right: 4px solid #27ae60;">right</div>
<div class="box" style="border-bottom: 4px solid #2980b9;">bottom</div>
<div class="box" style="border-left: 4px solid #8e44ad;">left</div>
</div>
<div class="row">
<div class="label">1 border 8px</div>
<div class="box" style="border-top: 8px solid #c0392b;">top</div>
<div class="box" style="border-right: 8px solid #1e8449;">right</div>
<div class="box" style="border-bottom: 8px solid #1f618d;">bottom</div>
<div class="box" style="border-left: 8px solid #6c3483;">left</div>
</div>
<!-- ============ 2 ADJACENT BORDERS ============ -->
<div class="row">
<div class="label">2 adj borders</div>
<div class="box" style="border-top: 4px solid #e74c3c; border-right: 4px solid #27ae60;">top+right</div>
<div class="box" style="border-right: 4px solid #27ae60; border-bottom: 4px solid #2980b9;">right+bot</div>
<div class="box" style="border-bottom: 4px solid #2980b9; border-left: 4px solid #8e44ad;">bot+left</div>
<div class="box" style="border-left: 4px solid #8e44ad; border-top: 4px solid #e74c3c;">left+top</div>
</div>
<div class="row">
<div class="label">2 adj 2px/8px</div>
<div class="box" style="border-top: 2px solid #e74c3c; border-right: 8px solid #27ae60;">t2/r8</div>
<div class="box" style="border-right: 8px solid #27ae60; border-bottom: 2px solid #2980b9;">r8/b2</div>
<div class="box" style="border-bottom: 2px solid #2980b9; border-left: 8px solid #8e44ad;">b2/l8</div>
<div class="box" style="border-left: 8px solid #8e44ad; border-top: 2px solid #e74c3c;">l8/t2</div>
</div>
<!-- ============ 2 OPPOSITE BORDERS ============ -->
<div class="row">
<div class="label">2 opp borders</div>
<div class="box" style="border-top: 4px solid #e74c3c; border-bottom: 4px solid #2980b9;">top+bot</div>
<div class="box" style="border-left: 4px solid #8e44ad; border-right: 4px solid #27ae60;">left+right</div>
<div class="box" style="border-top: 8px solid #c0392b; border-bottom: 2px solid #1f618d;">t8/b2</div>
<div class="box" style="border-left: 2px solid #6c3483; border-right: 8px solid #1e8449;">l2/r8</div>
</div>
<!-- ============ 3 BORDERS ============ -->
<div class="row">
<div class="label">3 borders</div>
<div class="box" style="border-top: 4px solid #e74c3c; border-right: 4px solid #27ae60; border-bottom: 4px solid #2980b9;">no left</div>
<div class="box" style="border-right: 4px solid #27ae60; border-bottom: 4px solid #2980b9; border-left: 4px solid #8e44ad;">no top</div>
<div class="box" style="border-bottom: 4px solid #2980b9; border-left: 4px solid #8e44ad; border-top: 4px solid #e74c3c;">no right</div>
<div class="box" style="border-left: 4px solid #8e44ad; border-top: 4px solid #e74c3c; border-right: 4px solid #27ae60;">no bottom</div>
</div>
<div class="row">
<div class="label">3 brdrs mixed</div>
<div class="box" style="border-top: 8px solid #c0392b; border-right: 2px solid #1e8449; border-bottom: 4px solid #1f618d;">t8/r2/b4</div>
<div class="box" style="border-right: 2px solid #27ae60; border-bottom: 8px solid #2980b9; border-left: 4px solid #6c3483;">r2/b8/l4</div>
<div class="box" style="border-bottom: 4px solid #2980b9; border-left: 8px solid #8e44ad; border-top: 2px solid #e74c3c;">b4/l8/t2</div>
<div class="box" style="border-left: 2px solid #8e44ad; border-top: 4px solid #e74c3c; border-right: 8px solid #27ae60;">l2/t4/r8</div>
</div>
<!-- ============ 4 BORDERS ============ -->
<div class="row">
<div class="label">4 borders</div>
<div class="box" style="border: 4px solid #555;">all 4px</div>
<div class="box" style="border: 1px solid #555;">all 1px</div>
<div class="box" style="border: 8px solid #555;">all 8px</div>
<div class="box" style="border-top: 2px solid #e74c3c; border-right: 4px solid #27ae60; border-bottom: 6px solid #2980b9; border-left: 8px solid #8e44ad;">mixed</div>
</div>
<div class="row">
<div class="label">4 brdrs colors</div>
<div class="box" style="border: 6px solid #e67e22;">orange</div>
<div class="box" style="border: 3px solid #1abc9c;">teal</div>
<div class="box" style="border: 10px solid #9b59b6;">purple</div>
<div class="box" style="border-top: 10px solid #f1c40f; border-right: 6px solid #e74c3c; border-bottom: 3px solid #2ecc71; border-left: 1px solid #3498db;">multi</div>
</div>
<!-- ============ 4 BORDERS + RADIUS ============ -->
<div class="row">
<div class="label">radius all same</div>
<div class="box" style="border: 4px solid #e74c3c; border-radius: 4px;">r4</div>
<div class="box" style="border: 4px solid #27ae60; border-radius: 8px;">r8</div>
<div class="box" style="border: 4px solid #2980b9; border-radius: 12px;">r12</div>
<div class="box" style="border: 4px solid #8e44ad; border-radius: 20px;">r20</div>
</div>
<div class="clearfix"></div>
</body>
</html>

View File

@@ -0,0 +1,86 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background-color: #f5f5f5;
font: 11px Verdana, Sans-Serif;
color: #333;
padding: 8px;
}
.row { clear: both; margin-bottom: 4px; }
.label {
float: left;
width: 130px;
font-size: 9px;
line-height: 1.2;
color: #555;
padding-top: 2px;
padding-right: 4px;
text-align: right;
}
.box {
float: left;
width: 48px;
height: 48px;
margin: 2px;
background-color: #e8f0f8;
font-size: 8px;
text-align: center;
padding: 4px;
overflow: hidden;
}
.clearfix { clear: both; }
</style>
</head>
<body>
<!-- ============ 4 BORDERS + RADIUS ============ -->
<div class="row">
<div class="label">radius diff</div>
<div class="box" style="border: 4px solid #e74c3c; border-top-left-radius: 16px; border-bottom-right-radius: 16px;">tl+br</div>
<div class="box" style="border: 4px solid #27ae60; border-top-right-radius: 16px; border-bottom-left-radius: 16px;">tr+bl</div>
<div class="box" style="border: 4px solid #2980b9; border-top-left-radius: 4px; border-top-right-radius: 8px; border-bottom-right-radius: 12px; border-bottom-left-radius: 2px;">mixed</div>
<div class="box" style="border: 4px solid #8e44ad; border-top-left-radius: 32px;">tl32</div>
</div>
<!-- ============ PARTIAL BORDERS + RADIUS ============ -->
<div class="row">
<div class="label">part+radius</div>
<div class="box" style="border-top: 4px solid #e74c3c; border-right: 4px solid #27ae60; border-radius: 8px;">t+r r8</div>
<div class="box" style="border-bottom: 4px solid #2980b9; border-left: 4px solid #8e44ad; border-radius: 12px;">b+l r12</div>
<div class="box" style="border-top: 4px solid #e74c3c; border-bottom: 4px solid #2980b9; border-radius: 8px;">t+b r8</div>
<div class="box" style="border: 4px solid #27ae60; border-left: 0px; border-radius: 8px;">noL r8</div>
</div>
<div class="row">
<div class="label">part+radius2</div>
<div class="box" style="border: 4px solid #e74c3c; border-bottom: 0px; border-radius: 8px;">noB r8</div>
<div class="box" style="border: 4px solid #2980b9; border-top: 0px; border-radius: 8px;">noT r8</div>
<div class="box" style="border: 4px solid #8e44ad; border-right: 0px; border-radius: 8px;">noR r8</div>
<div class="box" style="border-top: 2px solid #e74c3c; border-left: 6px solid #8e44ad; border-radius: 6px;">t2+l6</div>
</div>
<!-- ============ THICK BORDERS + RADIUS ============ -->
<div class="row">
<div class="label">thick+radius</div>
<div class="box" style="border: 8px solid #e74c3c; border-radius: 12px;">8px/r12</div>
<div class="box" style="border: 12px solid #27ae60; border-radius: 16px;">12/r16</div>
<div class="box" style="border: 3px solid #2980b9; border-radius: 32px;">3px/r32</div>
<div class="box" style="border-top: 12px solid #e74c3c; border-radius: 8px;">t12</div>
</div>
<!-- ============ DIFFERENT SIZES ============ -->
<div class="row">
<div class="label">large boxes</div>
<div class="box" style="width: 96px; height: 96px; border: 6px solid #e74c3c; border-radius: 16px;">96x96</div>
<div class="box" style="width: 96px; height: 48px; border: 4px solid #27ae60;">96x48</div>
<div class="box" style="width: 48px; height: 96px; border: 4px solid #2980b9; border-radius: 8px;">48x96</div>
<div class="box" style="width: 96px; height: 96px; border: 2px solid #e74c3c; border-top: 10px solid #8e44ad; border-left: 1px solid #2ecc71;">mixed96</div>
</div>
<div class="clearfix"></div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,128 @@
<!doctype html>
<html lang="en">
<head>
<style>
:root {
--bg-color: rgb(11, 26, 10);
--text-primary: #e5e5e5;
--text-secondary: #d4d4d4;
--text-muted: #a3a3a3;
--text-dim: #737373;
--accent: #fbbf24;
--border-color: #171717;
--white: #ffffff;
--font-main: "Fira Code", monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-color);
color: var(--text-primary);
font-family: var(--font-main);
-webkit-font-smoothing: antialiased;
min-height: 100vh;
line-height: 1.5;
}
.container {
max-width: 48rem;
margin: 0 auto;
padding: 6rem 1.5rem;
}
a {
color: inherit;
text-decoration: none;
transition: color 0.2s;
}
/* Post Content */
.prose {
color: var(--text-secondary);
max-width: none;
}
.prose h1 {
font-size: 2.25rem;
font-weight: 700;
margin-bottom: 1.5rem;
color: var(--white);
margin-top: 0;
line-height: 1.2;
}
.prose h2 {
font-size: 1.5rem;
font-weight: 700;
margin-top: 2rem;
margin-bottom: 1rem;
color: var(--white);
line-height: 1.3;
border-bottom: 4px solid #a98424;
display: inline-block;
margin-bottom: 30px;
}
.prose h3 {
font-size: 1.25rem;
font-weight: 700;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
color: var(--white);
}
.prose p {
margin-bottom: 1.25rem;
line-height: 1.75;
}
.prose hr {
margin: 2rem 0;
border: 1px solid transparent;
border-image: repeating-linear-gradient(
to right,
#b1b1b1 0 25px,
transparent 0px 40px
)
1;
}
.prose a {
color: var(--accent);
border-bottom: 2px solid rgba(251, 191, 36, 0.7);
transition: all 0.2s;
}
.prose a:hover {
border-bottom-color: var(--accent);
color: var(--accent);
}
</style>
</head>
<body>
<div class="container">
<article class="prose">
<h2 id="h2-wrap">It&#39;s Mostly Just Text and Media</h2>
<p>
Most apps are just that. Text and media in a never-ending,
all-consuming feed or a multi-page form, cleverly disguised by the
user interface.
</p>
<p>
Excluding heavy 3D gaming or utilities that genuinely require deep
integration with your phone&#39;s hardware (like accessing the LiDAR
scanner for AR), what are we actually left with? A thin client whose
main job is to fetch data from an API and render it onto native views.
</p>
<p>
Why do I need to download a 100+ MB app, give it permission to track
my location, and let it run background processes just to browse
through a restaurant menu, buy a ticket, or scroll through a list of
posts? At the end of the day, it is almost always just JSON being
parsed and rendered. Yet, companies insist on rebuilding their basic
content as native shells just to claim a permanent square of real
estate on my home screen.
</p>
<h2>The Enshittification Loop</h2>
<p>
When that full-screen modal pops up demanding you download the app to
read the rest of a thread, users choose the path of least resistance.
They download and they move on.
</p>
</article>
</div>
</body>
</html>

View File

@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>...::: Welcome to the Matrix :::...</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<meta name="google-site-verification" content="pQXoua1hj5cxrbwLlVLtAMaSo8WdqXmknunYi6-CLhA" />
<style type="text/css" media="screen">
body {
background: #000000;
color: #FFFFFF;
font-family: Verdana, Helvetica, Arial, sans-serif;
font-size: 8pt;
font-weight:normal;
text-align: left;
margin: 0;
padding: 0;
}
#logo {
width: 468px;
height: 120px;
border: 0px;
margin: 0 auto;
position:absolute;
left:50%;
margin-left: -234px;
}
#main {
width: 400px;
position:absolute;
left:50%;
margin-left: -200px;
margin-top: 120px;
border: 0px;
}
.box {
float: left;
clear:both;
width: 400px;
margin: 0 auto;
background-color: #333333;
text-align: center;
}
.folder_box {
float: left;
clear:both;
width: 400px;
margin: 0 auto;
text-align: left;
padding-top: 40px;
}
.inbox {
width: 100%;
height: 100px;
line-height:100px;
background-color: #666666;
}
.titlebox {
width: 100%;
height: 25px;
line-height: 25px;
font-weight: bold;
}
.login_inbox {
width: 100%;
height: 150px;
float:left;
background-color: #666666;
}
.user_inbox {
width: 100%;
height: 80px;
float:left;
background-color: #666666;
}
.loginbox {
width: 185px;
position:relative;
margin-left: auto;
margin-right: auto;
margin-top: 18px;
padding-bottom:5px;
padding-top:5px;
background-color: #333333;
font-size: 7pt;
clear:both;
}
.mini_titlebox {
width: 100%;
height: 25px;
line-height: 25px;
font-weight: bold;
}
a:link {
text-decoration: none;
color: #ffffff
}
a:visited {
text-decoration: none;
color: #ffffff
}
a:active {
text-decoration: none;
color: #ffffff
}
a:hover {
text-decoration: none;
background-color: #333333;
color: #ffffff
}
input {
font-family: Verdana, Arial, Helvetica, sans-serif;
font-size: 7pt;
background-color: #444444;
color: #FFFFFF;
border: 0;
height:12px;
}
input.user {
font-family: Verdana, Arial, Helvetica, sans-serif;
font-size: 7pt;
background-color: #444444;
color: #FFFFFF;
border: 0;
height:24px;
}
input.boton {
font-family: Verdana, Arial, Helvetica, sans-serif;
font-size: 8px;
color: #FFFFFF;
background-color: #333333;
height:16px;
}
</style>
</head>
<body>
<div id="main">
<div class="box">
<div class="titlebox">File Upload</div>
<div class="login_inbox">
<div class="loginbox">
<div class="mini_titlebox">[LOGIN]</div>
<form method="post" action="?s=1">
<p>Username: <input name="Nombre" type="text" size="12" /></p>
<p>Password: <input name="Password" type="password" size="12" /></p>
<p><input type="submit" name="Submit" value="Entrar" /></p>
</form>
</div>
</div>
</div>
<div class="box">
<div class="titlebox">Download Files</div>
<div class="inbox">
<a href="upload/">ENTER</a>
</div>
<div class="titlebox"></div>
</div>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,56 @@
<!doctype html>
<html lang="en">
<head>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #1e2a38;
font:
16px Verdana,
Sans-Serif;
color: #d0d8e0;
line-height: 1.5;
padding: 20px;
}
.padded-span {
background-color: #c06060;
color: #ffffff;
padding: 10px 20px;
margin: 0 5px;
}
.large-padding {
background-color: #60c060;
color: #ffffff;
padding: 30px;
margin: 0 10px;
}
.mixed-padding {
background-color: #6060c0;
color: #ffffff;
padding-left: 50px;
padding-right: 10px;
padding-top: 5px;
padding-bottom: 20px;
}
</style>
</head>
<body>
<div>
This is normal text with a <span class="padded-span">padded span</span> inside it.
</div>
<div style="margin-top: 40px;">
Here is a <span class="large-padding">large padded span</span> which should increase the line height and spacing.
</div>
<div style="margin-top: 40px;">
Finally, a <span class="mixed-padding">mixed padding span</span> to test asymmetrical padding values.
</div>
</body>
</html>

View File

@@ -246,6 +246,8 @@ class EE_API FontTrueType : public Font {
mutable UnorderedMap<unsigned int, unsigned int> mClosestCharacterSize;
mutable UnorderedMap<Uint32, Uint32> mCodePointIndexCache;
mutable UnorderedMap<Uint32, std::tuple<Uint32, Uint32, bool>> mKeyCache;
mutable UnorderedMap<Uint64, Float> mKerningCache; // For codepoints (getKerning)
mutable UnorderedMap<Uint64, Float> mKerningGlyphCache; // For glyph indices
FontHinting mHinting{ FontHinting::Full };
FontAntialiasing mAntialiasing{ FontAntialiasing::Grayscale };
FontTrueType* mFontBold{ nullptr };

View File

@@ -2,7 +2,6 @@
#include <eepp/config.hpp>
#include <eepp/graphics/fontstyleconfig.hpp>
#include <vector>
namespace EE::Graphics {
@@ -11,12 +10,12 @@ enum class LineWrapMode { NoWrap, Letter, Word };
enum class LineWrapType { Viewport, LineBreakingColumn };
struct LineWrapInfo {
std::vector<Int64> wraps; // Each wrap character position (where the wrap must happen)
Float paddingStart{ 0 }; // Padding of the wrapped lines
SmallVector<Int64, 4> wraps; // Each wrap character position (where the wrap must happen)
Float paddingStart{ 0 }; // Padding of the wrapped lines
};
struct LineWrapInfoEx : public LineWrapInfo {
std::vector<Float> wrapsWidth; // Each wrap width
SmallVector<Float, 4> wrapsWidth; // Each wrap width
};
class EE_API LineWrap {

View File

@@ -3,6 +3,7 @@
#include <eepp/graphics/drawable.hpp>
#include <eepp/graphics/text.hpp>
#include <eepp/ui/csslayouttypes.hpp>
#include <memory>
#include <variant>
#include <vector>
@@ -33,6 +34,9 @@ class EE_API RichText : public Drawable {
*/
void addSpan( const String& text, const FontStyleConfig& style );
void addSpan( const String& text, const FontStyleConfig& style, const Rectf& margin,
const Rectf& padding );
/**
* @brief Adds a text span with individual style parameters.
* @param text The text content.
@@ -80,9 +84,17 @@ class EE_API RichText : public Drawable {
struct CustomBlock {
Sizef size;
bool isBlock{ false };
UI::CSSFloat floatType{ UI::CSSFloat::None };
UI::CSSClear clearType{ UI::CSSClear::None };
};
using Block = std::variant<std::shared_ptr<Text>, std::shared_ptr<Drawable>, CustomBlock>;
struct SpanBlock {
std::shared_ptr<Text> text;
Rectf margin;
Rectf padding;
};
using Block = std::variant<SpanBlock, std::shared_ptr<Drawable>, CustomBlock>;
/**
* @brief Adds a drawable (e.g., an image) into the text flow.
@@ -95,7 +107,9 @@ class EE_API RichText : public Drawable {
* @param size The physical dimensions of the spacer.
* @param isBlock Whether this spacer acts as a block-level element.
*/
void addCustomSize( const Sizef& size, bool isBlock = false );
void addCustomSize( const Sizef& size, bool isBlock = false,
UI::CSSFloat floatType = UI::CSSFloat::None,
UI::CSSClear clearType = UI::CSSClear::None );
/** @return The list of blocks. */
const std::vector<Block>& getBlocks() { return mBlocks; }

View File

@@ -258,7 +258,7 @@ class EE_API Text {
void setShadowColor( const Color& color );
/** @return Every cached text line width */
const std::vector<Float>& getLinesWidth();
const SmallVector<Float, 4>& getLinesWidth();
/** @return The last line width */
Float getLastLineWidth();
@@ -413,8 +413,8 @@ class EE_API Text {
TextDirection mDirection{ TextDirection::Unspecified };
Vector2f mInitialOffset{ 0.f, 0.f };
mutable std::vector<Int64> mVisualLines;
mutable std::vector<Float> mLinesWidth;
mutable SmallVector<Int64, 4> mVisualLines;
mutable SmallVector<Float, 4> mLinesWidth;
std::vector<VertexCoords> mVertices;
std::vector<Color> mColors;

View File

@@ -30,7 +30,7 @@ class EE_API TextLayout {
bool isRTL() const { return direction == TextDirection::RightToLeft; }
std::vector<Float> getLinesWidth() const;
SmallVector<Float, 4> getLinesWidth() const;
static Cache layout( const String& string, Font* font, const Uint32& fontSize,
const Uint32& style, const Uint32& tabWidth = 4,

View File

@@ -1,6 +1,7 @@
#ifndef EEPP_NETWORK_HPP
#define EEPP_NETWORK_HPP
#include <eepp/network/cookiemanager.hpp>
#include <eepp/network/ftp.hpp>
#include <eepp/network/http.hpp>
#include <eepp/network/ipaddress.hpp>

View File

@@ -0,0 +1,46 @@
#ifndef EE_NETWORK_COOKIEMANAGER_HPP
#define EE_NETWORK_COOKIEMANAGER_HPP
#include <eepp/config.hpp>
#include <eepp/network/http.hpp>
#include <map>
#include <string>
namespace EE { namespace Network {
class EE_API CookieManager {
public:
CookieManager();
/** Store Set-Cookie headers from an HTTP response for the given domain. */
void storeCookies( const std::string& domain, const Http::Response& response );
/** Store cookies from a raw Set-Cookie header string. */
void storeCookiesFromHeader( const std::string& domain, const std::string& setCookieHeader );
/** Build the Cookie header string for outgoing requests to the given domain. */
std::string getCookieHeader( const std::string& domain ) const;
/** Remove all stored cookies. */
void clear();
/** @return The number of cookie entries across all domains. */
size_t size() const;
/** @return true if no cookies are stored. */
bool empty() const;
/** @return true if the domain has cookies */
bool hasCookie( const std::string& domain ) const;
protected:
mutable Mutex mMutex;
UnorderedMap<std::string, std::map<std::string, std::string>> mCookies;
void parseSetCookie( const std::string& domain, const std::string& setCookieHeader );
};
}} // namespace EE::Network
#endif

View File

@@ -49,8 +49,9 @@ class EE_API Http : NonCopyable {
MultipleChoices = 300, ///< The requested page can be accessed from several locations
MovedPermanently = 301, ///< The requested page has permanently moved to a new location
MovedTemporarily = 302, ///< The requested page has temporarily moved to a new location
NotModified = 304, ///< For conditional requests, means the requested page hasn't
///< changed and doesn't need to be refreshed
SeeOther = 303, ///< The response can be found under a different URI using a GET method
NotModified = 304, ///< For conditional requests, means the requested page hasn't
///< changed and doesn't need to be refreshed
TemporaryRedirect = 307, ///< The requested page has temporarily moved to a new location
PermanentRedirect = 308, ///< The requested page has permanently moved to a new location
@@ -96,6 +97,8 @@ class EE_API Http : NonCopyable {
FieldTable getHeaders();
const FieldTable& getHeaders() const;
/** @brief Get the value of a field
** If the field @a field is not found in the response header,
** the empty string is returned. This function uses
@@ -180,7 +183,7 @@ class EE_API Http : NonCopyable {
///< target resource.
Patch, ///< The PATCH method is used to apply partial modifications to a resource.
Connect ///< The CONNECT method starts two-way communications with the requested
///< resource. It can be used to open a tunnel.
///< resource. It can be used to open a tunnel.
};
/** @brief Enumerate the available states for a request */
@@ -188,7 +191,8 @@ class EE_API Http : NonCopyable {
Connected, ///< Connected to server.
Sent, ///< Request sent to the server.
HeaderReceived, ///< Header received.
ContentReceived ///< Content received.
ContentReceived, ///< Content received.
Redirect, ///< A redirect has been handled
};
static std::string statusToString( Status status );
@@ -199,6 +203,9 @@ class EE_API Http : NonCopyable {
/** @return The method string from a method */
static std::string methodToString( const Method& method );
static Method getRedirectMethodFromStatus( Method requestMethod,
Response::Status responseStatus );
/** @brief Default constructor
** This constructor creates a GET request, with the root
** URI ("/") and an empty body.
@@ -675,25 +682,29 @@ class EE_API Http : NonCopyable {
const Request::ProgressCallback& progressCallback = Request::ProgressCallback(),
const Request::FieldTable& headers = Request::FieldTable(),
const std::string& body = "", const bool& validateCertificate = true,
const URI& proxy = URI() );
const URI& proxy = URI(), bool followRedirect = true );
/** Creates an async HTTP GET Request using the global HTTP Client Pool
** @return The unique async request id
*/
static Uint64 getAsync(
const Http::AsyncResponseCallback& cb, const URI& uri, const Time& timeout = Time::Zero,
const Request::ProgressCallback& progressCallback = Request::ProgressCallback(),
const Request::FieldTable& headers = Request::FieldTable(), const std::string& body = "",
const bool& validateCertificate = true, const URI& proxy = URI() );
static Uint64
getAsync( const Http::AsyncResponseCallback& cb, const URI& uri,
const Time& timeout = Time::Zero,
const Request::ProgressCallback& progressCallback = Request::ProgressCallback(),
const Request::FieldTable& headers = Request::FieldTable(),
const std::string& body = "", const bool& validateCertificate = true,
const URI& proxy = URI(), bool followRedirect = true );
/** Creates an async HTTP POST Request using the global HTTP Client Pool
** @return The unique async request id
*/
static Uint64 postAsync(
const Http::AsyncResponseCallback& cb, const URI& uri, const Time& timeout = Time::Zero,
const Request::ProgressCallback& progressCallback = Request::ProgressCallback(),
const Request::FieldTable& headers = Request::FieldTable(), const std::string& body = "",
const bool& validateCertificate = true, const URI& proxy = URI() );
static Uint64
postAsync( const Http::AsyncResponseCallback& cb, const URI& uri,
const Time& timeout = Time::Zero,
const Request::ProgressCallback& progressCallback = Request::ProgressCallback(),
const Request::FieldTable& headers = Request::FieldTable(),
const std::string& body = "", const bool& validateCertificate = true,
const URI& proxy = URI(), bool followRedirect = true );
/** It will try to get the proxy from the environment variables. */
static URI getEnvProxyURI();

View File

@@ -122,6 +122,10 @@ class EE_API Event {
OnFoldUnfoldRange,
OnResourceLoaded,
OnDiscard,
OnNavigationStarted,
OnNavigationCompleted,
OnNavigationError,
OnTitleChanged,
NoEvent = eeINDEX_NOT_FOUND
};

View File

@@ -79,7 +79,8 @@ enum NodeFlags {
NODE_FLAG_LOADING = ( 1 << 27 ),
NODE_FLAG_CLOSING_CHILDREN = ( 1 << 28 ),
NODE_FLAG_DISABLE_CLICK_FOCUS = ( 1 << 29 ),
NODE_FLAG_FREE_USE = ( 1 << 30 )
NODE_FLAG_TEXTNODE = ( 1 << 30 ),
NODE_FLAG_FREE_USE = ( 1 << 31 )
};
/**
@@ -209,6 +210,9 @@ class EE_API Node : public Transformable {
*/
virtual bool isType( const Uint32& type ) const;
/** @return True if this node is a UITextNode, false otherwise. */
bool isTextNode() const;
/**
* @brief Posts a message to this node and its ancestors.
*
@@ -1773,6 +1777,13 @@ class EE_API Node : public Transformable {
*/
bool isClosing() const;
/**
* @brief Checks if the node is marked for closure or any node in its parent tree.
*
* @return True if node is about to close
*/
bool inClosingTree() const;
/**
* @brief Checks if the node is in the process of closing children.
*

View File

@@ -1,11 +1,11 @@
#ifndef EE_SYSTEM_BASE64_HPP
#define EE_SYSTEM_BASE64_HPP
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <eepp/config.hpp>
#include <string>
#include <string_view>
namespace EE { namespace System {
@@ -13,25 +13,29 @@ class EE_API Base64 {
public:
/** Encode binary data into base64 digits with MIME style === pads
** @return The final length of the output */
static int encode( size_t in_len, const unsigned char* in, size_t out_len, char* out );
static size_t encode( size_t in_len, const unsigned char* in, size_t out_len, char* out );
/** Decode base64 digits with MIME style === pads into binary data
** @return The final length of the output */
static int decode( size_t in_len, const char* in, size_t out_len, unsigned char* out );
static size_t decode( size_t in_len, const char* in, size_t out_len, unsigned char* out );
/** Encodes a string into a base64 string
** @return True if encoding was successful */
static bool encode( const std::string& in, std::string& out );
static bool encode( std::string_view in, std::string& out );
/** Decodes a base64 string to a string
** @return True if encoding was successful */
static bool decode( const std::string& in, std::string& out );
static size_t decode( std::string_view in, std::string& out );
/** @return A safe encoding output length for an input of the length indicated */
static inline int encodeSafeOutLen( size_t in_len ) { return in_len / 3 * 4 + 4 + 1; }
static inline size_t encodeSafeOutLen( size_t in_len ) {
return ( ( in_len + 2 ) / 3 ) * 4 + 1;
}
/** @return A safe decoding output length for an input of the length indicated */
static inline int decodeSafeOutLen( size_t in_len ) { return in_len / 4 * 3 + 1; }
static inline size_t decodeSafeOutLen( size_t in_len ) {
return ( ( in_len + 3 ) / 4 ) * 3 + 1;
}
};
}} // namespace EE::System

View File

@@ -4,6 +4,7 @@
#include <eepp/ui/abstract/uiabstracttableview.hpp>
#include <eepp/ui/abstract/uiabstractview.hpp>
#include <eepp/ui/base.hpp>
#include <eepp/ui/blocklayouter.hpp>
#include <eepp/ui/border.hpp>
#include <eepp/ui/colorschemepreferences.hpp>
#include <eepp/ui/css/animationdefinition.hpp>
@@ -29,6 +30,7 @@
#include <eepp/ui/css/stylesheetvariable.hpp>
#include <eepp/ui/css/timingfunction.hpp>
#include <eepp/ui/css/transitiondefinition.hpp>
#include <eepp/ui/csslayouttypes.hpp>
#include <eepp/ui/doc/documentview.hpp>
#include <eepp/ui/doc/foldrangeservice.hpp>
#include <eepp/ui/doc/foldrangetype.hpp>
@@ -45,10 +47,8 @@
#include <eepp/ui/doc/textposition.hpp>
#include <eepp/ui/doc/textrange.hpp>
#include <eepp/ui/doc/textundostack.hpp>
#include <eepp/ui/htmlinput.hpp>
#include <eepp/ui/htmltextarea.hpp>
#include <eepp/ui/htmltextinput.hpp>
#include <eepp/ui/iconmanager.hpp>
#include <eepp/ui/inlinelayouter.hpp>
#include <eepp/ui/keyboardshortcut.hpp>
#include <eepp/ui/models/csspropertiesmodel.hpp>
#include <eepp/ui/models/filesystemmodel.hpp>
@@ -64,7 +64,9 @@
#include <eepp/ui/models/variant.hpp>
#include <eepp/ui/models/widgettreemodel.hpp>
#include <eepp/ui/mouseshortcut.hpp>
#include <eepp/ui/nonelayouter.hpp>
#include <eepp/ui/splitdirection.hpp>
#include <eepp/ui/tablelayouter.hpp>
#include <eepp/ui/tools/htmlformatter.hpp>
#include <eepp/ui/tools/textureatlaseditor.hpp>
#include <eepp/ui/tools/uiaudioplayer.hpp>
@@ -91,13 +93,22 @@
#include <eepp/ui/uifontstyleconfig.hpp>
#include <eepp/ui/uigridlayout.hpp>
#include <eepp/ui/uihelper.hpp>
#include <eepp/ui/uihtmlform.hpp>
#include <eepp/ui/uihtmlimage.hpp>
#include <eepp/ui/uihtmlinput.hpp>
#include <eepp/ui/uihtmllistitem.hpp>
#include <eepp/ui/uihtmltable.hpp>
#include <eepp/ui/uihtmltextarea.hpp>
#include <eepp/ui/uihtmltextinput.hpp>
#include <eepp/ui/uihtmlwidget.hpp>
#include <eepp/ui/uiicon.hpp>
#include <eepp/ui/uiicontheme.hpp>
#include <eepp/ui/uiiconthememanager.hpp>
#include <eepp/ui/uiimage.hpp>
#include <eepp/ui/uiitemcontainer.hpp>
#include <eepp/ui/uilayout.hpp>
#include <eepp/ui/uilayouter.hpp>
#include <eepp/ui/uilayoutermanager.hpp>
#include <eepp/ui/uilinearlayout.hpp>
#include <eepp/ui/uilistbox.hpp>
#include <eepp/ui/uilistboxitem.hpp>
@@ -129,6 +140,7 @@
#include <eepp/ui/uiscrollablewidget.hpp>
#include <eepp/ui/uiscrollbar.hpp>
#include <eepp/ui/uiscrollview.hpp>
#include <eepp/ui/uiwebview.hpp>
#include <eepp/ui/uiselectbutton.hpp>
#include <eepp/ui/uiskin.hpp>
#include <eepp/ui/uiskinstate.hpp>
@@ -140,6 +152,7 @@
#include <eepp/ui/uistackwidget.hpp>
#include <eepp/ui/uistate.hpp>
#include <eepp/ui/uistyle.hpp>
#include <eepp/ui/uisvg.hpp>
#include <eepp/ui/uitab.hpp>
#include <eepp/ui/uitablecell.hpp>
#include <eepp/ui/uitableheadercolumn.hpp>
@@ -148,6 +161,7 @@
#include <eepp/ui/uitabwidget.hpp>
#include <eepp/ui/uitextedit.hpp>
#include <eepp/ui/uitextinput.hpp>
#include <eepp/ui/uitextnode.hpp>
#include <eepp/ui/uitextspan.hpp>
#include <eepp/ui/uitextureregion.hpp>
#include <eepp/ui/uitextview.hpp>

View File

@@ -0,0 +1,28 @@
#ifndef EE_UI_BLOCKLAYOUTER_HPP
#define EE_UI_BLOCKLAYOUTER_HPP
#include <eepp/ui/uilayouter.hpp>
namespace EE::Graphics {
class RichText;
}
using namespace EE::Graphics;
namespace EE { namespace UI {
class EE_API BlockLayouter : public UILayouter {
public:
BlockLayouter( UIWidget* container ) : UILayouter( container ) {}
void updateLayout() override;
void computeIntrinsicWidths() override;
Float getMinIntrinsicWidth() override;
Float getMaxIntrinsicWidth() override;
protected:
void positionRichTextChildren( RichText* rt );
};
}} // namespace EE::UI
#endif

View File

@@ -230,15 +230,34 @@ enum class PropertyId : Uint32 {
DisplayOptions = String::hash( "display-options" ),
MenuWidthMode = String::hash( "menu-width-mode" ),
ExpandText = String::hash( "expand-text" ),
Colspan = String::hash( "colspan" ),
ColSpan = String::hash( "colspan" ),
TableLayout = String::hash( "table-layout" ),
Cellpadding = String::hash( "cellpadding" ),
Cellspacing = String::hash( "cellspacing" ),
CellPadding = String::hash( "cellpadding" ),
CellSpacing = String::hash( "cellspacing" ),
Size = String::hash( "size" ),
Type = String::hash( "type" ),
Rows = String::hash( "rows" ),
Cols = String::hash( "cols" ),
InputMode = String::hash( "input-mode" ),
Hidden = String::hash( "hidden" ),
Display = String::hash( "display" ),
Position = String::hash( "position" ),
Top = String::hash( "top" ),
Right = String::hash( "right" ),
Bottom = String::hash( "bottom" ),
Left = String::hash( "left" ),
ZIndex = String::hash( "z-index" ),
ListStyleType = String::hash( "list-style-type" ),
ListStylePosition = String::hash( "list-style-position" ),
ListStyleImage = String::hash( "list-style-image" ),
Float = String::hash( "float" ),
Clear = String::hash( "clear" ),
DataLanguage = String::hash( "data-language" ), // Minor hack
Action = String::hash( "action" ),
Method = String::hash( "method" ),
Enctype = String::hash( "enctype" ),
Overflow = String::hash( "overflow" ),
Target = String::hash( "target" ),
};
enum class PropertyType : Uint32 {

View File

@@ -24,7 +24,8 @@ enum class ShorthandId : Uint32 {
BorderWidth = String::hash( "border-width" ),
BorderRadius = String::hash( "border-radius" ),
MinSize = String::hash( "min-size" ),
MaxSize = String::hash( "max-size" )
MaxSize = String::hash( "max-size" ),
Font = String::hash( "font" )
};
typedef std::function<std::vector<StyleSheetProperty>( const ShorthandDefinition* shorthand,

View File

@@ -0,0 +1,82 @@
#ifndef EE_UI_CSSLAYOUTTYPES_HPP
#define EE_UI_CSSLAYOUTTYPES_HPP
#include <eepp/config.hpp>
#include <string>
namespace EE { namespace UI {
enum class CSSDisplay {
Inline,
Block,
InlineBlock,
ListItem,
Flex,
None,
Table,
TableRow,
TableCell,
TableHead,
TableBody,
TableFooter
};
struct EE_API CSSDisplayHelper {
static std::string toString( CSSDisplay display );
static CSSDisplay fromString( std::string_view val );
};
enum class CSSPosition { Static, Relative, Absolute, Fixed, Sticky };
struct EE_API CSSPositionHelper {
static std::string toString( CSSPosition position );
static CSSPosition fromString( std::string_view val );
};
enum class CSSListStyleType {
None,
Disc,
Circle,
Square,
Decimal,
LowerAlpha,
UpperAlpha,
LowerRoman,
UpperRoman
};
struct EE_API CSSListStyleTypeHelper {
static std::string toString( CSSListStyleType type );
static CSSListStyleType fromString( std::string_view val );
};
enum class CSSListStylePosition { Outside, Inside };
struct EE_API CSSListStylePositionHelper {
static std::string toString( CSSListStylePosition pos );
static CSSListStylePosition fromString( std::string_view val );
};
enum class CSSFloat { None, Left, Right };
struct EE_API CSSFloatHelper {
static std::string toString( CSSFloat val );
static CSSFloat fromString( std::string_view val );
};
enum class CSSClear { None, Left, Right, Both };
struct EE_API CSSClearHelper {
static std::string toString( CSSClear val );
static CSSClear fromString( std::string_view val );
};
}} // namespace EE::UI
#endif

View File

@@ -63,7 +63,7 @@ class EE_API DocumentView {
void updateCache( Int64 fromLine, Int64 toLine, Int64 numLines );
Config getConfig() const { return mConfig; }
const Config& getConfig() const { return mConfig; }
void setConfig( Config config );

View File

@@ -260,7 +260,9 @@ constexpr auto SyntaxStyleEmpty() {
*/
class EE_API SyntaxColorScheme {
public:
static SyntaxColorScheme getDefault();
static SyntaxColorScheme getDefaultDark();
static SyntaxColorScheme getDefaultLight();
static std::vector<SyntaxColorScheme> loadFromStream( IOStream& stream );

View File

@@ -658,7 +658,7 @@ class EE_API TextDocument {
TextPosition getMatchingBracket( TextPosition startPosition,
const String::StringBaseType& openBracket,
const String::StringBaseType& closeBracket, MatchDirection dir,
bool allowDepth = true );
bool allowDepth = true, Time timeout = Time::Zero );
TextRange getMatchingBracket( TextPosition startPosition, const String& openBracket,
const String& closeBracket, MatchDirection dir,

View File

@@ -0,0 +1,17 @@
#ifndef EE_UI_INLINELAYOUTER_HPP
#define EE_UI_INLINELAYOUTER_HPP
#include <eepp/ui/uilayouter.hpp>
namespace EE { namespace UI {
class EE_API InlineLayouter : public UILayouter {
public:
InlineLayouter( UIWidget* container ) : UILayouter( container ) {}
void updateLayout() override {}
void computeIntrinsicWidths() override {}
};
}} // namespace EE::UI
#endif

View File

@@ -0,0 +1,17 @@
#ifndef EE_UI_NONELAYOUTER_HPP
#define EE_UI_NONELAYOUTER_HPP
#include <eepp/ui/uilayouter.hpp>
namespace EE { namespace UI {
class EE_API NoneLayouter : public UILayouter {
public:
NoneLayouter( UIWidget* container ) : UILayouter( container ) {}
void updateLayout() override {}
void computeIntrinsicWidths() override {}
};
}} // namespace EE::UI
#endif

View File

@@ -0,0 +1,59 @@
#ifndef EE_UI_TABLELAYOUTER_HPP
#define EE_UI_TABLELAYOUTER_HPP
#include <eepp/core/small_vector.hpp>
#include <eepp/ui/uilayouter.hpp>
namespace EE { namespace UI {
class UIHTMLTableRow;
class UIHTMLTableCell;
class UIHTMLTableHead;
class UIHTMLTableBody;
class UIHTMLTableFooter;
enum class TableLayout { Auto, Fixed };
class EE_API TableLayouter : public UILayouter {
public:
TableLayouter( UIWidget* container ) : UILayouter( container ) {}
void updateLayout() override;
void computeIntrinsicWidths() override;
void setTableLayout( TableLayout layout );
TableLayout getTableLayout() const;
void setCellPadding( Float padding );
Float getCellPadding() const;
void setCellSpacing( Float spacing );
Float getCellSpacing() const;
Float getMinIntrinsicWidth() override;
Float getMaxIntrinsicWidth() override;
protected:
SmallVector<UIHTMLTableRow*> mRows;
SmallVector<Float> mColWidths;
SmallVector<UIHTMLTableCell*> mCells;
SmallVector<Uint32> mRowCellOffsets;
SmallVector<Float> mColMinWidths;
SmallVector<Float> mColMaxWidths;
SmallVector<Float> mColSpecifiedWidths;
TableLayout mTableLayout{ TableLayout::Auto };
UIHTMLTableHead* mHead{ nullptr };
UIHTMLTableBody* mBody{ nullptr };
UIHTMLTableFooter* mFooter{ nullptr };
Float mCellpadding{ 0 };
Float mCellspacing{ 0 };
};
}} // namespace EE::UI
#endif

View File

@@ -88,13 +88,13 @@ class EE_API UIBorderDrawable : public Drawable {
protected:
const UINode* mOwner;
VertexBuffer* mVertexBuffer;
Borders mBorders;
mutable Borders mBorders;
BorderStr mBorderStr;
BorderType mBorderType;
Sizef mSize;
bool mNeedsUpdate;
bool mColorNeedsUpdate;
bool mHasBorder;
mutable bool mHasBorder;
bool mSmooth{ false };
virtual void onAlphaChange();
@@ -105,7 +105,7 @@ class EE_API UIBorderDrawable : public Drawable {
void update();
void updateBorders();
void updateBorders() const;
};
}} // namespace EE::UI

View File

@@ -187,6 +187,10 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client {
static UICodeEditor* New();
static UICodeEditor* NewWithTag( const std::string& tag,
const bool& autoRegisterBaseCommands = true,
const bool& autoRegisterBaseKeybindings = true );
static UICodeEditor* NewOpt( const bool& autoRegisterBaseCommands,
const bool& autoRegisterBaseKeybindings );
@@ -844,6 +848,14 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client {
size_t getTotalVisibleLines() const;
bool usesDefaultStyle() const { return mUseDefaultStyle; }
void setUseDefaultStyle( bool use );
bool dynamicTheming() const { return mDynamicTheming; }
void setDynamicTheming( bool set );
protected:
struct LastXOffset {
TextPosition position{ 0, 0 };
@@ -893,6 +905,7 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client {
bool mTabStops{ false };
bool mKerningEnabled{ false };
bool mDisableScrollInvalidation{ false };
bool mDynamicTheming{ false };
DocumentView mDocView;
Clock mBlinkTimer;
Time mBlinkTime;
@@ -1219,6 +1232,10 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client {
virtual void onAutoSize();
virtual void onClassChange();
inline bool needsHorizontalLength() const;
void updateDynamicTheme();
};
}} // namespace EE::UI

View File

@@ -113,6 +113,7 @@ enum UINodeType {
UI_TYPE_TEXTSPAN,
UI_TYPE_RICHTEXT,
UI_TYPE_MARKDOWNVIEW,
UI_TYPE_HTML_WIDGET,
UI_TYPE_HTML_TABLE,
UI_TYPE_HTML_TABLE_HEAD,
UI_TYPE_HTML_TABLE_BODY,
@@ -127,6 +128,12 @@ enum UINodeType {
UI_TYPE_BR,
UI_TYPE_HTML_HTML,
UI_TYPE_HTML_BODY,
UI_TYPE_HTML_LIST_ITEM,
UI_TYPE_HTML_IMAGE,
UI_TYPE_HTML_FORM,
UI_TYPE_WEBVIEW,
UI_TYPE_SVG,
UI_TYPE_TEXTNODE,
UI_TYPE_MODULES = 10000,
UI_TYPE_TERMINAL = 10001,
UI_TYPE_USER = 200000,

View File

@@ -0,0 +1,56 @@
#ifndef EE_UI_UIHTMLFORM_HPP
#define EE_UI_UIHTMLFORM_HPP
#include <string>
#include <utility>
#include <vector>
#include <eepp/ui/uirichtext.hpp>
namespace EE { namespace UI {
class UISceneNode;
class EE_API UIHTMLForm : public UIRichText {
public:
static UIHTMLForm* New();
UIHTMLForm( const std::string& tag = "form" );
virtual Uint32 getType() const;
virtual bool isType( const Uint32& type ) const;
virtual bool applyProperty( const StyleSheetProperty& attribute );
virtual std::string getPropertyString( const PropertyDefinition* propertyDef,
const Uint32& propertyIndex = 0 ) const;
virtual std::vector<PropertyId> getPropertiesImplemented() const;
void submit();
const std::string& getAction() const { return mAction; }
void setAction( const std::string& action ) { mAction = action; }
const std::string& getMethod() const { return mMethod; }
void setMethod( const std::string& method ) { mMethod = method; }
const std::string& getEnctype() const { return mEnctype; }
void setEnctype( const std::string& enctype ) { mEnctype = enctype; }
protected:
std::string mAction;
std::string mMethod{ "GET" };
std::string mEnctype{ "application/x-www-form-urlencoded" };
virtual Uint32 onMessage( const NodeMessage* msg );
static void collectFormData( Node* node,
std::vector<std::pair<std::string, std::string>>& fields );
bool isSubmitTrigger( Node* sender ) const;
};
}} // namespace EE::UI
#endif

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

View File

@@ -1,15 +1,15 @@
#ifndef EE_UI_HTMLINPUT_HPP
#define EE_UI_HTMLINPUT_HPP
#ifndef EE_UI_UIHTMLINPUT_HPP
#define EE_UI_UIHTMLINPUT_HPP
#include <eepp/ui/uiwidget.hpp>
namespace EE { namespace UI {
class EE_API HTMLInput : public UIWidget {
class EE_API UIHTMLInput : public UIWidget {
public:
static HTMLInput* New();
static UIHTMLInput* New();
HTMLInput();
UIHTMLInput();
virtual Uint32 getType() const;
@@ -32,10 +32,13 @@ class EE_API HTMLInput : public UIWidget {
UIWidget* getChildWidget() const;
String getFormValue() const;
protected:
std::string mInputType{ "text" };
UIWidget* mChildWidget{ nullptr };
std::map<PropertyId, StyleSheetProperty> mProperties;
String mValue;
void createChildWidget();

View File

@@ -0,0 +1,51 @@
#ifndef EE_UI_UIHTMLLISTITEM_HPP
#define EE_UI_UIHTMLLISTITEM_HPP
#include <eepp/graphics/text.hpp>
#include <eepp/ui/uirichtext.hpp>
#include <memory>
namespace EE { namespace UI {
class EE_API UIHTMLListItem : public UIRichText {
public:
static UIHTMLListItem* New();
virtual Uint32 getType() const;
virtual bool isType( const Uint32& type ) const;
virtual void draw();
virtual bool applyProperty( const StyleSheetProperty& attribute );
virtual std::string getPropertyString( const PropertyDefinition* propertyDef,
const Uint32& propertyIndex = 0 ) const;
virtual std::vector<PropertyId> getPropertiesImplemented() const;
CSSListStyleType getListStyleType() const { return mListStyleType; }
void setListStyleType( CSSListStyleType type );
CSSListStylePosition getListStylePosition() const { return mListStylePosition; }
void setListStylePosition( CSSListStylePosition pos );
protected:
UIHTMLListItem();
CSSListStyleType mListStyleType{ CSSListStyleType::None };
CSSListStylePosition mListStylePosition{ CSSListStylePosition::Outside };
std::unique_ptr<Graphics::Text> mListMarkerText;
int countPrecedingLiSiblings() const;
String::View getListMarkerString() const;
void invalidateList();
};
}} // namespace EE::UI
#endif

View File

@@ -2,64 +2,43 @@
#define EE_UI_UIHTMLTABLE_HPP
#include <eepp/core/small_vector.hpp>
#include <eepp/ui/uilayout.hpp>
#include <eepp/ui/uihtmlwidget.hpp>
#include <eepp/ui/uirichtext.hpp>
namespace EE { namespace UI {
class UIHTMLTableRow;
class UIHTMLTableCell;
class UIHTMLTableHead;
class UIHTMLTableBody;
class UIHTMLTableFooter;
enum class TableLayout { Auto, Fixed };
class EE_API UIHTMLTable : public UILayout {
class EE_API UIHTMLTable : public UIHTMLWidget {
public:
friend class TableLayouter;
static UIHTMLTable* New();
UIHTMLTable();
void setTableLayout( TableLayout layout );
TableLayout getTableLayout() const;
virtual Uint32 getType() const;
virtual bool isType( const Uint32& type ) const;
virtual void updateLayout();
virtual Float getMinIntrinsicWidth() const;
virtual Float getMaxIntrinsicWidth() const;
virtual std::vector<PropertyId> getPropertiesImplemented() const;
virtual std::string getPropertyString( const PropertyDefinition* propertyDef,
const Uint32& state = 0 ) const;
virtual bool applyProperty( const StyleSheetProperty& attribute );
protected:
virtual Uint32 onMessage( const NodeMessage* Msg );
void computeIntrinsicWidths() const;
SmallVector<UIHTMLTableRow*> mRows;
SmallVector<Float> mColWidths;
SmallVector<UIHTMLTableCell*> mCells;
SmallVector<Uint32> mRowCellOffsets;
mutable SmallVector<Float> mColMinWidths;
mutable SmallVector<Float> mColMaxWidths;
mutable SmallVector<Float> mColSpecifiedWidths;
TableLayout mTableLayout{ TableLayout::Auto };
mutable UIHTMLTableHead* mHead{ nullptr };
mutable UIHTMLTableBody* mBody{ nullptr };
mutable UIHTMLTableFooter* mFooter{ nullptr };
Float mCellpadding{ 0 };
Float mCellspacing{ 0 };
};
class EE_API UIHTMLTableCell : public UIRichText {
public:
friend class UIHTMLTable;
friend class UIHTMLTable;
friend class TableLayouter;
static UIHTMLTableCell* New( const std::string& tag );
@@ -69,17 +48,22 @@ class EE_API UIHTMLTableCell : public UIRichText {
virtual bool isType( const Uint32& type ) const;
virtual std::vector<PropertyId> getPropertiesImplemented() const;
virtual std::string getPropertyString( const PropertyDefinition* propertyDef,
const Uint32& state = 0 ) const;
virtual bool applyProperty( const StyleSheetProperty& attribute );
Uint32 getColspan() const;
Uint32 getColSpan() const;
virtual void onSizeChange();
protected:
Uint32 mColspan{ 1 };
Uint32 mColSpan{ 1 };
};
class EE_API UIHTMLTableRow : public UIWidget {
class EE_API UIHTMLTableRow : public UIHTMLWidget {
public:
static UIHTMLTableRow* New();
@@ -90,7 +74,7 @@ class EE_API UIHTMLTableRow : public UIWidget {
virtual bool isType( const Uint32& type ) const;
};
class EE_API UIHTMLTableHead : public UIWidget {
class EE_API UIHTMLTableHead : public UIHTMLWidget {
public:
static UIHTMLTableHead* New();
@@ -101,7 +85,7 @@ class EE_API UIHTMLTableHead : public UIWidget {
virtual bool isType( const Uint32& type ) const;
};
class EE_API UIHTMLTableFooter : public UIWidget {
class EE_API UIHTMLTableFooter : public UIHTMLWidget {
public:
static UIHTMLTableFooter* New();
@@ -112,7 +96,7 @@ class EE_API UIHTMLTableFooter : public UIWidget {
virtual bool isType( const Uint32& type ) const;
};
class EE_API UIHTMLTableBody : public UIWidget {
class EE_API UIHTMLTableBody : public UIHTMLWidget {
public:
static UIHTMLTableBody* New();

View File

@@ -1,15 +1,15 @@
#ifndef EE_UI_HTMLTEXTAREA_HPP
#define EE_UI_HTMLTEXTAREA_HPP
#ifndef EE_UI_UIHTMLTEXTAREA_HPP
#define EE_UI_UIHTMLTEXTAREA_HPP
#include <eepp/ui/uitextedit.hpp>
namespace EE { namespace UI {
class EE_API HTMLTextArea : public UITextEdit {
class EE_API UIHTMLTextArea : public UITextEdit {
public:
static HTMLTextArea* New();
static UIHTMLTextArea* New();
HTMLTextArea();
UIHTMLTextArea();
virtual Uint32 getType() const;

View File

@@ -5,11 +5,11 @@
namespace EE { namespace UI {
class EE_API HTMLTextInput : public UITextInput {
class EE_API UIHTMLTextInput : public UITextInput {
public:
static HTMLTextInput* New();
static UIHTMLTextInput* New();
HTMLTextInput();
UIHTMLTextInput();
virtual Uint32 getType() const;
@@ -33,7 +33,7 @@ class EE_API HTMLTextInput : public UITextInput {
void setHtmlSize( Uint32 size );
protected:
HTMLTextInput( const std::string& tag );
UIHTMLTextInput( const std::string& tag );
Uint32 mHtmlSize{ 20 };
bool mPacking{ false };

View File

@@ -0,0 +1,91 @@
#ifndef EE_UI_UIHTMLWIDGET_HPP
#define EE_UI_UIHTMLWIDGET_HPP
#include <eepp/ui/csslayouttypes.hpp>
#include <eepp/ui/uilayout.hpp>
namespace EE { namespace Graphics {
class RichText;
}} // namespace EE::Graphics
namespace EE { namespace UI {
class UILayouter;
class EE_API UIHTMLWidget : public UILayout {
public:
static UIHTMLWidget* New();
UIHTMLWidget( const std::string& tag = "htmlwidget" );
virtual ~UIHTMLWidget();
virtual Uint32 getType() const;
virtual bool isType( const Uint32& type ) const;
UILayouter* getLayouter();
virtual bool isPacking() const;
virtual void onDisplayChange();
CSSDisplay getDisplay() const { return mDisplay; }
void setDisplay( CSSDisplay display );
CSSPosition getCSSPosition() const { return mPosition; }
void setCSSPosition( CSSPosition position );
CSSFloat getCSSFloat() const { return mFloat; }
void setCSSFloat( CSSFloat cssFloat );
CSSClear getCSSClear() const { return mClear; }
void setCSSClear( CSSClear cssClear );
const Rectf& getOffsets() const { return mOffsets; }
void setOffsets( const Rectf& offsets );
int getZIndex() const { return mZIndex; }
void setZIndex( int zIndex );
virtual std::vector<PropertyId> getPropertiesImplemented() const;
virtual std::string getPropertyString( const PropertyDefinition* propertyDef,
const Uint32& state = 0 ) const;
virtual bool applyProperty( const StyleSheetProperty& attribute );
virtual void updateLayout();
UIWidget* getContainingBlock();
void positionOutOfFlowChildren();
virtual RichText* getRichTextPtr() { return nullptr; }
virtual bool isMergeable() const { return false; }
virtual String getFormValue() const { return String(); }
virtual void invalidateIntrinsicSize();
bool isOutOfFlow() const;
protected:
CSSDisplay mDisplay{ CSSDisplay::Block };
CSSPosition mPosition{ CSSPosition::Static };
CSSFloat mFloat{ CSSFloat::None };
CSSClear mClear{ CSSClear::None };
std::string mTopEq{ "auto" };
std::string mRightEq{ "auto" };
std::string mBottomEq{ "auto" };
std::string mLeftEq{ "auto" };
Rectf mOffsets{ 0, 0, 0, 0 };
int mZIndex{ 0 };
UILayouter* mLayouter{ nullptr };
UnorderedMap<std::string, StyleSheetProperty> mDataProperties;
};
}} // namespace EE::UI
#endif

View File

@@ -19,12 +19,17 @@ class EE_API UILayout : public UIWidget {
void setGravityOwner( bool gravityOwner );
bool isPacking() const { return mPacking; }
virtual bool isPacking() const { return mPacking; }
bool isLayoutDirty() const { return mDirtyLayout; }
void onAutoSizeChild( UIWidget* child );
void setLayoutDirty();
protected:
friend class UISceneNode;
friend class UILayouter;
UnorderedSet<UILayout*> mLayouts;
bool mDirtyLayout{ false };
@@ -49,11 +54,7 @@ class EE_API UILayout : public UIWidget {
virtual void updateLayoutWrappingContents();
void setLayoutDirty();
bool setMatchParentIfNeededVerticalGrowth();
void onAutoSizeChild( UIWidget* child );
};
}} // namespace EE::UI

View File

@@ -0,0 +1,37 @@
#ifndef EE_UI_UILAYOUTER_HPP
#define EE_UI_UILAYOUTER_HPP
#include <cstddef>
#include <eepp/config.hpp>
namespace EE { namespace UI {
class UIWidget;
class EE_API UILayouter {
public:
UILayouter( UIWidget* container ) : mContainer( container ) {}
virtual ~UILayouter() {}
virtual void updateLayout() = 0;
virtual void computeIntrinsicWidths() {}
virtual Float getMinIntrinsicWidth() { return 0; }
virtual Float getMaxIntrinsicWidth() { return 0; }
virtual void invalidateIntrinsicWidths() { mIntrinsicWidthsDirty = true; }
virtual bool isPacking() const { return mPacking; }
protected:
UIWidget* mContainer;
bool mPacking{ false };
size_t mResizedCount{ 0 };
bool mIntrinsicWidthsDirty{ true };
Float mMinIntrinsicWidth{ 0 };
Float mMaxIntrinsicWidth{ 0 };
void setMatchParentIfNeededVerticalGrowth();
};
}} // namespace EE::UI
#endif

View File

@@ -0,0 +1,19 @@
#ifndef EE_UI_UILAYOUTERMANAGER_HPP
#define EE_UI_UILAYOUTERMANAGER_HPP
#include <eepp/config.hpp>
#include <eepp/ui/csslayouttypes.hpp>
namespace EE { namespace UI {
class UILayouter;
class UIWidget;
class EE_API UILayouterManager {
public:
static UILayouter* create( CSSDisplay display, UIWidget* container );
};
}} // namespace EE::UI
#endif

View File

@@ -32,6 +32,9 @@ class UIWidget;
class EE_API UINode : public Node {
public:
friend class BlockLayouter;
friend class InlineLayouter;
friend class TableLayouter;
/**
* @brief Creates a new UINode instance.
*
@@ -1419,6 +1422,9 @@ class EE_API UINode : public Node {
*/
virtual bool isScrollable() const;
/** @brief Get a widget's computed absolute font size in pixels. */
Float getAbsoluteFontSize( const UIWidget* widget ) const;
protected:
Vector2f mDpPos;
Sizef mDpSize;
@@ -1859,8 +1865,6 @@ class EE_API UINode : public Node {
* @return The droppable hover color.
*/
Color getDroppableHoveringColor();
Float getAbsoluteFontSize( const UIWidget* widget ) const;
};
}} // namespace EE::UI

View File

@@ -89,6 +89,8 @@ class EE_API UIPushButton : public UIWidget {
UIPushButton* setExpandTextView( bool expand );
virtual void loadFromXmlNode( const pugi::xml_node& node );
protected:
UIImage* mIcon;
UITextView* mTextBox;

View File

@@ -2,12 +2,18 @@
#define EE_UI_UIRICHTEXT_HPP
#include <eepp/graphics/richtext.hpp>
#include <eepp/ui/uihtmlwidget.hpp>
#include <eepp/ui/uilayout.hpp>
namespace EE { namespace UI {
class EE_API UIRichText : public UILayout {
class EE_API UIRichText : public UIHTMLWidget {
public:
enum class IntrinsicMode { None, Min, Max };
static void rebuildRichText( UILayout* container, RichText& richText,
IntrinsicMode mode = IntrinsicMode::None );
static UIRichText* New();
static UIRichText* NewWithTag( const std::string& tag );
@@ -38,8 +44,6 @@ class EE_API UIRichText : public UILayout {
static UIRichText* NewPre() { return UIRichText::NewWithTag( "pre" ); };
static UIRichText* NewListItem() { return UIRichText::NewWithTag( "li" ); };
static UIRichText* NewBlockquote() { return UIRichText::NewWithTag( "blockquote" ); };
virtual Uint32 getType() const;
@@ -125,14 +129,13 @@ class EE_API UIRichText : public UILayout {
String getSelectionString() const;
virtual void updateLayout();
virtual RichText* getRichTextPtr() { return &mRichText; }
protected:
RichText mRichText;
Int64 mSelCurInit{ 0 };
Int64 mSelCurEnd{ 0 };
bool mSelecting{ false };
size_t mResizedCount{ 0 };
explicit UIRichText( const std::string& tag = "richtext" );
@@ -147,7 +150,6 @@ class EE_API UIRichText : public UILayout {
virtual void onChildCountChange( Node* child, const bool& removed );
virtual void onFontChanged();
virtual void onFontStyleChanged();
virtual void onAlphaChange();
virtual void onSelectionChange();
void selCurInit( const Int64& init );
@@ -155,9 +157,7 @@ class EE_API UIRichText : public UILayout {
Int64 selCurInit() const { return mSelCurInit; }
Int64 selCurEnd() const { return mSelCurEnd; }
enum class IntrinsicMode { None, Min, Max };
void rebuildRichText( RichText& richText, IntrinsicMode mode = IntrinsicMode::None );
void positionChildren();
void updateDefaultSpansStyle();
};
@@ -166,6 +166,7 @@ class EE_API UIHTMLHtml : public UIRichText {
static UIHTMLHtml* New( const std::string& tag );
virtual Uint32 getType() const override;
bool isType( const Uint32& type ) const override;
bool applyProperty( const StyleSheetProperty& attribute ) override;
protected:
UIHTMLHtml( const std::string& tag = "html" );
@@ -184,6 +185,18 @@ class EE_API UIHTMLBody : public UIRichText {
UIHTMLBody( const std::string& tag = "body" );
};
class EE_API UILineBreak : public UIRichText {
public:
static UILineBreak* New( const std::string& tag );
virtual Uint32 getType() const;
bool isType( const Uint32& type ) const;
protected:
UILineBreak( const std::string& tag = "br" );
};
}} // namespace EE::UI
#endif

View File

@@ -3,6 +3,7 @@
#include <eepp/network/uri.hpp>
#include <eepp/scene/scenenode.hpp>
#include <eepp/network/cookiemanager.hpp>
#include <eepp/system/threadpool.hpp>
#include <eepp/system/translator.hpp>
#include <eepp/ui/colorschemepreferences.hpp>
@@ -27,6 +28,13 @@ class UIWidget;
class UILayout;
class UIIcon;
struct NavigationRequest {
URI uri;
std::string method{ "GET" };
std::string body;
std::map<std::string, std::string> extraHeaders;
};
class EE_API UISceneNode : public SceneNode {
public:
/**
@@ -704,10 +712,14 @@ class EE_API UISceneNode : public SceneNode {
/** Handles opening an specific URI */
void openURL( URI uri );
/* Sets a callback to intercept the openURL calls, returns true if intercepted, false to leave
* the default openURL implementation handle it.
*/
void setURLInterceptorCb( std::function<bool( URI uri )> cb ) { mURLInterceptorCb = cb; };
/** Handles navigation (GET/POST) with request body and custom headers. */
void navigate( const NavigationRequest& request );
/** Sets a callback to intercept navigate() calls. Return true to handle the request,
* false to fall through to the URL interceptor and default handling. */
void setNavigationInterceptorCb( std::function<bool( const NavigationRequest& request )> cb ) {
mNavigationInterceptorCb = cb;
};
/**
* Solves a relative path with no scheme or authority into a complete URI.
@@ -718,6 +730,10 @@ class EE_API UISceneNode : public SceneNode {
/** @return The document referer */
URI getReferer() const { return mReferer; };
const Network::CookieManager& getCookieManager() const { return mCookieManager; }
Network::CookieManager& getCookieManager() { return mCookieManager; }
protected:
friend class EE::UI::UIWindow;
friend class EE::UI::UIWidget;
@@ -747,7 +763,8 @@ class EE_API UISceneNode : public SceneNode {
std::shared_ptr<ThreadPool> mThreadPool;
URI mURI;
URI mReferer;
std::function<bool( URI uri )> mURLInterceptorCb;
std::function<bool( const NavigationRequest& request )> mNavigationInterceptorCb;
Network::CookieManager mCookieManager;
/**
* @brief Protected constructor.

View File

@@ -74,6 +74,8 @@ class EE_API UIScrollView : public UITouchDraggableWidget {
Uint32 mParentSizeChangeCb{ 0 };
Uint32 mParentCloseCb{ 0 };
UIScrollView( const std::string& tag );
UIScrollView();
virtual Uint32 onMessage( const NodeMessage* Msg );

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

@@ -10,6 +10,8 @@ class EE_API UITextEdit : public UICodeEditor {
public:
static UITextEdit* New();
static UITextEdit* NewWithTag( const std::string& tag );
virtual ~UITextEdit();
virtual Uint32 getType() const;
@@ -24,11 +26,11 @@ class EE_API UITextEdit : public UICodeEditor {
void setWordWrap( bool enabled );
protected:
UITextEdit();
virtual bool applyProperty( const StyleSheetProperty& attribute );
protected:
UITextEdit( const std::string& tag );
virtual void drawCursor( const Vector2f& startScroll, const Float& lineHeight,
const TextPosition& cursor );
};

View File

@@ -0,0 +1,35 @@
#ifndef EE_UI_UITEXTNODE_HPP
#define EE_UI_UITEXTNODE_HPP
#include <eepp/ui/uiwidget.hpp>
namespace EE { namespace UI {
class EE_API UITextNode : public UIWidget {
public:
static UITextNode* New();
UITextNode();
virtual ~UITextNode();
virtual Uint32 getType() const;
virtual bool isType( const Uint32& type ) const;
virtual void draw();
virtual std::string getPropertyString( const PropertyDefinition* propertyDef,
const Uint32& propertyIndex = 0 ) const;
const String& getText() const;
void setText( const String& text );
protected:
String mText;
};
}} // namespace EE::UI
#endif

View File

@@ -2,13 +2,14 @@
#define EE_UI_UITEXTSPAN_HPP
#include <eepp/ui/uifontstyleconfig.hpp>
#include <eepp/ui/uirichtext.hpp>
#include <eepp/ui/uiwidget.hpp>
namespace EE { namespace UI {
using SpanHitBoxes = SmallVector<Rectf, 4>;
class EE_API UITextSpan : public UIWidget {
class EE_API UITextSpan : public UIRichText {
public:
static UITextSpan* New();
@@ -32,12 +33,16 @@ class EE_API UITextSpan : public UIWidget {
static UITextSpan* NewCode() { return NewWithTag( "code" ); }
static UITextSpan* NewSmall() { return NewWithTag( "small" ); }
virtual ~UITextSpan();
virtual Uint32 getType() const;
virtual bool isType( const Uint32& type ) const;
virtual bool isMergeable() const;
virtual void draw();
virtual bool applyProperty( const StyleSheetProperty& attribute );
@@ -51,7 +56,7 @@ class EE_API UITextSpan : public UIWidget {
UITextSpan* setText( const String& text );
const UIFontStyleConfig& getFontStyleConfig() const;
const FontStyleConfig& getFontStyleConfig() const;
virtual void loadFromXmlNode( const pugi::xml_node& node );
@@ -97,7 +102,7 @@ class EE_API UITextSpan : public UIWidget {
UITextSpan* setFontShadowOffset( const Vector2f& offset );
void setInheritedStyle( const UIFontStyleConfig& fontStyleConfig );
void setInheritedStyle( const FontStyleConfig& fontStyleConfig );
enum StyleState {
StyleStateNone = 0,
@@ -134,21 +139,18 @@ class EE_API UITextSpan : public UIWidget {
protected:
Uint32 mStyleState{ StyleStateNone };
String mText;
UIFontStyleConfig mFontStyleConfig;
SpanHitBoxes mHitBoxes;
explicit UITextSpan( const std::string& tag = "span" );
virtual void drawBorder();
virtual void onTextChanged();
virtual void onFontChanged();
virtual void onFontStyleChanged();
virtual void onAlphaChange();
virtual void onChildCountChange( Node* child, const bool& removed );
virtual Uint32 onMessage( const NodeMessage* Msg );
};
@@ -171,6 +173,7 @@ class EE_API UIAnchorSpan : public UITextSpan {
UIAnchorSpan( const std::string& tag = "a" );
std::string mHref;
std::string mTarget;
virtual Uint32 onKeyDown( const KeyEvent& event );

View File

@@ -0,0 +1,114 @@
#ifndef EE_UIWEBVIEW_HPP
#define EE_UIWEBVIEW_HPP
#include <eepp/network/http.hpp>
#include <eepp/network/uri.hpp>
#include <eepp/scene/event.hpp>
#include <eepp/system/time.hpp>
#include <eepp/ui/uiscrollview.hpp>
#include <functional>
#include <string>
#include <vector>
using namespace EE::Network;
namespace EE { namespace UI {
class UIHTMLHtml;
class UIHTMLBody;
class EE_API UIWebView : public UIScrollView {
public:
struct NavigationEvent : Scene::Event {
URI uri;
bool success{ false };
std::string error;
NavigationEvent( Node* node, const Uint32& eventType, const URI& ruri, bool succ = false,
std::string err = "" ) :
Scene::Event( node, eventType ),
uri( ruri ),
success( succ ),
error( std::move( err ) ) {}
};
static UIWebView* New();
virtual ~UIWebView();
virtual Uint32 getType() const;
virtual bool isType( const Uint32& type ) const;
void loadURI( URI uri );
void loadURI( URI uri, const std::string& method, const std::string& body,
const Http::Request::FieldTable& headers );
void goHistoryBack();
void goHistoryForward();
bool canGoBack() const;
bool canGoForward() const;
const std::vector<URI>& getHistory() const;
int getHistoryIndex() const;
const URI& getCurrentURI() const;
void reload();
UIWidget* getDocumentContainer() const;
void setStyleSheetDefaultMarker( Uint32 marker );
void setUserAgent( const std::string& userAgent );
const std::string& getUserAgent() const;
void setDefaultTimeout( const Time& timeout );
Uint32 onNavigationStarted( std::function<void( const URI& )> cb );
Uint32 onNavigationCompleted( std::function<void( const URI& )> cb );
Uint32 onNavigationError( std::function<void( const URI&, const std::string& )> cb );
Uint32 onTitleChanged( std::function<void( const std::string& )> cb );
protected:
UIWebView();
UIWidget* mDocContainer{ nullptr };
std::vector<URI> mHistory;
int mHistoryIndex{ -1 };
bool mIsLoading{ false };
std::string mUserAgent;
Time mDefaultTimeout{ Seconds( 30 ) };
Uint32 mStyleSheetDefaultMarker{ 0 };
void loadURI( URI uri, bool isHistoryNav );
void loadURI( URI uri, bool isHistoryNav, const std::string& method, const std::string& body,
const Http::Request::FieldTable& headers );
virtual void onSizeChange();
void loadDocumentData( URI url, std::string data );
void
loadDocumentAsync( const URI& url, const std::string& method = "GET",
const std::string& body = "",
const Http::Request::FieldTable& headers = Http::Request::FieldTable() );
void pushHistory( const URI& url );
void navigateToHistoryIndex( int index );
void updateHTMLMinHeight( UIHTMLHtml* html, UIHTMLBody* body );
void updateHTMLMinHeightForDocument();
};
}} // namespace EE::UI
#endif

View File

@@ -21,6 +21,13 @@ namespace EE { namespace UI {
class UITooltip;
class UIStyle;
struct MarginAuto {
static constexpr auto Left = ( 1 << 0 );
static constexpr auto Right = ( 1 << 1 );
static constexpr auto Top = ( 1 << 2 );
static constexpr auto Bottom = ( 1 << 3 );
};
/**
* @brief Base class for all UI widgets in the eepp framework.
*
@@ -531,7 +538,7 @@ class EE_API UIWidget : public UINode {
* Forces a recalculation of the intrinsic widths on the next call to
* getMinIntrinsicWidth() or getMaxIntrinsicWidth().
*/
void invalidateIntrinsicSize();
virtual void invalidateIntrinsicSize();
/**
* @brief Loads widget configuration from an XML node.
@@ -611,6 +618,15 @@ class EE_API UIWidget : public UINode {
*/
const Rectf& getPixelsPadding() const;
/**
* @brief Gets the content offset area (padding + border).
*
* Returns a Rectf containing padding + border for all 4 sides.
*
* @return The content offset as a Rectf.
*/
Rectf getPixelsContentOffset() const;
/**
* @brief Sets the padding for all sides.
*
@@ -784,6 +800,21 @@ class EE_API UIWidget : public UINode {
*/
std::vector<const char*> getStyleSheetPseudoClassesStrings() const;
/** @return True if the widget is not a text node. */
bool isWidgetElement() const;
/** @return The index of this element among its sibling elements. */
Uint32 getElementIndex() const;
/** @return The index of this element among its sibling elements of the same type. */
Uint32 getElementOfTypeIndex() const;
/** @return The number of child elements. */
Uint32 getChildElementCount() const;
/** @return The number of child elements of the specified type. */
Uint32 getChildElementOfTypeCount( const Uint32& type ) const;
/**
* @brief Resets all CSS classes and removes them.
*
@@ -1315,10 +1346,21 @@ class EE_API UIWidget : public UINode {
*/
virtual void onWidgetCreated();
/**@return The property `width` converted as length */
Float getPropertyWidth() const;
/**@return The property `height` converted as length */
Float getPropertyHeight() const;
/* @return The width of the widget when size policy is match_parent */
Float getMatchParentWidth() const;
/* @return The height of the widget when size policy is match_parent */
Float getMatchParentHeight() const;
/* @return The size of the widget when size policy is match_parent */
Sizef getSizeFromLayoutPolicy();
protected:
friend class UIManager;
friend class UISceneNode;
@@ -1350,11 +1392,6 @@ class EE_API UIWidget : public UINode {
mutable bool mIntrinsicWidthsDirty{ true };
Uint8 mMarginAuto{ 0 };
static constexpr Uint8 MarginAutoLeft = ( 1 << 0 );
static constexpr Uint8 MarginAutoRight = ( 1 << 1 );
static constexpr Uint8 MarginAutoTop = ( 1 << 2 );
static constexpr Uint8 MarginAutoBottom = ( 1 << 3 );
void calculateAutoMargin();
/**
@@ -1698,15 +1735,6 @@ class EE_API UIWidget : public UINode {
*/
void reloadFontFamily();
/* @return The width of the widget when size policy is match_parent */
Float getMatchParentWidth() const;
/* @return The height of the widget when size policy is match_parent */
Float getMatchParentHeight() const;
/* @return The size of the widget when size policy is match_parent */
Sizef getSizeFromLayoutPolicy();
UIWidget* setLayoutMarginAuto( Uint32 dir, bool isAuto );
};

View File

@@ -4,10 +4,10 @@
#include <eepp/config.hpp>
#include <string>
#define EEPP_MAJOR_VERSION 2
#define EEPP_MINOR_VERSION 9
#define EEPP_PATCH_LEVEL 2
#define EEPP_CODENAME "Sādhanā"
#define EEPP_MAJOR_VERSION 3
#define EEPP_MINOR_VERSION 0
#define EEPP_PATCH_LEVEL 0
#define EEPP_CODENAME "Khaya"
/** The compiled version of the library */
#define EEPP_VERSION( x ) \

View File

@@ -5,7 +5,7 @@ cd ../../bin/unit_tests
echo "=== Running eepp unit tests under GDB (xvfb) ==="
xvfb-run -s "-screen 0 1280x1024x24" \
ASAN_OPTIONS=detect_leaks=0 xvfb-run -s "-screen 0 1280x1024x24" \
gdb --batch --quiet --return-child-result \
-ex "set confirm off" \
-ex "set print thread-events off" \

View File

@@ -0,0 +1,190 @@
#include <eepp/ui/csslayouttypes.hpp>
namespace EE { namespace UI {
std::string CSSDisplayHelper::toString( CSSDisplay display ) {
switch ( display ) {
case CSSDisplay::Inline:
return "inline";
case CSSDisplay::InlineBlock:
return "inline-block";
case CSSDisplay::ListItem:
return "list-item";
case CSSDisplay::Flex:
return "flex";
case CSSDisplay::None:
return "none";
case CSSDisplay::Table:
return "table";
case CSSDisplay::TableRow:
return "table-row";
case CSSDisplay::TableCell:
return "table-cell";
case CSSDisplay::TableHead:
return "table-header-group";
case CSSDisplay::TableBody:
return "table-row-group";
case CSSDisplay::TableFooter:
return "table-footer-group";
case CSSDisplay::Block:
default:
return "block";
}
};
CSSDisplay CSSDisplayHelper::fromString( std::string_view val ) {
CSSDisplay display = CSSDisplay::Block;
if ( val == "inline" )
display = CSSDisplay::Inline;
else if ( val == "inline-block" )
display = CSSDisplay::InlineBlock;
else if ( val == "list-item" )
display = CSSDisplay::ListItem;
else if ( val == "none" )
display = CSSDisplay::None;
else if ( val == "table" )
display = CSSDisplay::Table;
else if ( val == "table-row" )
display = CSSDisplay::TableRow;
else if ( val == "table-cell" )
display = CSSDisplay::TableCell;
else if ( val == "table-header-group" )
display = CSSDisplay::TableHead;
else if ( val == "table-row-group" )
display = CSSDisplay::TableBody;
else if ( val == "table-footer-group" )
display = CSSDisplay::TableFooter;
else if ( val == "flex" )
display = CSSDisplay::Flex;
return display;
}
std::string CSSPositionHelper::toString( CSSPosition position ) {
switch ( position ) {
case CSSPosition::Relative:
return "relative";
case CSSPosition::Absolute:
return "absolute";
case CSSPosition::Fixed:
return "fixed";
case CSSPosition::Sticky:
return "sticky";
case CSSPosition::Static:
default: {
}
}
return "static";
}
CSSPosition CSSPositionHelper::fromString( std::string_view val ) {
CSSPosition position = CSSPosition::Static;
if ( val == "relative" )
position = CSSPosition::Relative;
else if ( val == "absolute" )
position = CSSPosition::Absolute;
else if ( val == "fixed" )
position = CSSPosition::Fixed;
else if ( val == "sticky" )
position = CSSPosition::Sticky;
return position;
}
std::string CSSListStyleTypeHelper::toString( CSSListStyleType type ) {
switch ( type ) {
case CSSListStyleType::Disc:
return "disc";
case CSSListStyleType::Circle:
return "circle";
case CSSListStyleType::Square:
return "square";
case CSSListStyleType::Decimal:
return "decimal";
case CSSListStyleType::LowerAlpha:
return "lower-alpha";
case CSSListStyleType::UpperAlpha:
return "upper-alpha";
case CSSListStyleType::LowerRoman:
return "lower-roman";
case CSSListStyleType::UpperRoman:
return "upper-roman";
case CSSListStyleType::None:
default:
return "none";
}
}
CSSListStyleType CSSListStyleTypeHelper::fromString( std::string_view val ) {
if ( val == "disc" )
return CSSListStyleType::Disc;
if ( val == "circle" )
return CSSListStyleType::Circle;
if ( val == "square" )
return CSSListStyleType::Square;
if ( val == "decimal" )
return CSSListStyleType::Decimal;
if ( val == "lower-alpha" )
return CSSListStyleType::LowerAlpha;
if ( val == "upper-alpha" )
return CSSListStyleType::UpperAlpha;
if ( val == "lower-roman" )
return CSSListStyleType::LowerRoman;
if ( val == "upper-roman" )
return CSSListStyleType::UpperRoman;
return CSSListStyleType::None;
}
std::string CSSListStylePositionHelper::toString( CSSListStylePosition pos ) {
return pos == CSSListStylePosition::Inside ? "inside" : "outside";
}
CSSListStylePosition CSSListStylePositionHelper::fromString( std::string_view val ) {
if ( val == "inside" )
return CSSListStylePosition::Inside;
return CSSListStylePosition::Outside;
}
std::string CSSFloatHelper::toString( CSSFloat val ) {
switch ( val ) {
case CSSFloat::Left:
return "left";
case CSSFloat::Right:
return "right";
case CSSFloat::None:
default:
return "none";
}
}
CSSFloat CSSFloatHelper::fromString( std::string_view val ) {
if ( val == "left" )
return CSSFloat::Left;
if ( val == "right" )
return CSSFloat::Right;
return CSSFloat::None;
}
std::string CSSClearHelper::toString( CSSClear val ) {
switch ( val ) {
case CSSClear::Left:
return "left";
case CSSClear::Right:
return "right";
case CSSClear::Both:
return "both";
case CSSClear::None:
default:
return "none";
}
}
CSSClear CSSClearHelper::fromString( std::string_view val ) {
if ( val == "left" )
return CSSClear::Left;
if ( val == "right" )
return CSSClear::Right;
if ( val == "both" )
return CSSClear::Both;
return CSSClear::None;
}
}} // namespace EE::UI

View File

@@ -78,16 +78,14 @@ static Drawable* parseDataURI( const std::string& name ) {
format.svgScale( PixelDensity::getPixelDensity() );
if ( decodingType == "base64" ) {
int fileStart = formatAndEncSep + 1;
int base64Size = name.size() - fileStart;
int bufSize = Base64::decodeSafeOutLen( base64Size );
if ( bufSize <= 0 )
return nullptr;
ScopedBuffer buffer( bufSize );
int len = Base64::decode( base64Size, &name[fileStart], bufSize, buffer.get() );
if ( len > 0 )
std::string_view fileBase64 = std::string_view{ name }.substr( fileStart );
std::string buffer;
int len = Base64::decode( fileBase64, buffer );
if ( len > 0 ) {
tex = TextureFactory::instance()->loadFromMemory(
buffer.get(), len, false, Texture::ClampMode::ClampToEdge, false, false,
format );
(const unsigned char*)buffer.c_str(), buffer.size(), false,
Texture::ClampMode::ClampToEdge, false, false, format );
}
} else if ( decodingType == "urldecode" ) {
int fileStart = formatAndEncSep + 1;
std::string decoded( URI::decode( name.substr( fileStart ) ) );

View File

@@ -235,6 +235,22 @@ static inline Uint64 getCodePointKey( Uint32 codePoint, bool bold, bool italics,
( static_cast<EE::Uint64>( italics ) << 32 ) | codePoint;
}
// Combine kerning parameters into a single 64-bit key for O(1) lookups.
// - first/index1 : 21 bits (covers full Unicode range up to 0x10FFFF)
// - second/index2: 21 bits
// - characterSize: 12 bits (max font size 4095)
// - bold : 1 bit
// - italic : 1 bit
// - outline : 8 bits (max outline thickness 2.55 scaled by 100)
static inline Uint64 getKerningKey( Uint32 first, Uint32 second, unsigned int characterSize,
bool bold, bool italic, Float outlineThickness ) {
return ( static_cast<Uint64>( first & 0x1FFFFF ) << 43 ) |
( static_cast<Uint64>( second & 0x1FFFFF ) << 22 ) |
( static_cast<Uint64>( characterSize & 0xFFF ) << 10 ) |
( static_cast<Uint64>( bold ) << 9 ) | ( static_cast<Uint64>( italic ) << 8 ) |
( static_cast<Uint64>( static_cast<Uint32>( outlineThickness * 100.f ) & 0xFF ) );
}
FontTrueType* FontTrueType::New( const std::string& FontName ) {
return eeNew( FontTrueType, ( FontName ) );
}
@@ -455,7 +471,7 @@ bool FontTrueType::setFontFace( void* _face ) {
if ( mHasSvgGlyphs ) {
#ifdef EE_TRUETYPE_SVG_FONT_ENABLED
FT_Property_Set( static_cast<FT_Library>( mLibrary ), "ot-svg", "svg-hooks", &svg_hooks );
FT_Property_Set( static_cast<FT_Library>( mLibrary ), "ot-svg", "svg-hooks", &svg_hooks );
#else
return false;
#endif
@@ -641,7 +657,6 @@ Glyph FontTrueType::getGlyphByIndex( Uint32 index, unsigned int characterSize, b
GlyphDrawable* FontTrueType::getGlyphDrawable( Uint32 codePoint, unsigned int characterSize,
bool bold, bool italic,
Float outlineThickness ) const {
// mKeyCache
Page& page = getPage( characterSize );
GlyphDrawableTable& drawables = page.drawables;
@@ -800,44 +815,45 @@ Float FontTrueType::getKerning( Uint32 first, Uint32 second, unsigned int charac
if ( first == 0 || second == 0 || isMonospace() )
return 0.f;
Uint64 key = getKerningKey( first, second, characterSize, bold, italic, outlineThickness );
auto it = mKerningCache.find( key );
if ( it != mKerningCache.end() ) {
return it->second;
}
Float kerningVal = 0.f;
FT_Face face = static_cast<FT_Face>( mFace );
if ( face && setCurrentSize( characterSize ) ) {
auto glyph1 = getGlyph( first, characterSize, bold, italic, outlineThickness );
auto glyph2 = getGlyph( second, characterSize, bold, italic, outlineThickness );
if ( glyph1.font != glyph2.font )
return 0.f;
// Convert the characters to indices
FT_UInt index1 = getGlyphIndex( first );
FT_UInt index2 = getGlyphIndex( second );
// Retrieve position compensation deltas generated by FT_LOAD_FORCE_AUTOHINT flag
auto firstRsbDelta = static_cast<Float>( glyph1.rsbDelta );
auto secondLsbDelta = static_cast<Float>( glyph2.lsbDelta );
// Get the kerning vector
FT_Vector kerning;
kerning.x = kerning.y = 0;
if ( glyph1.font == glyph2.font ) {
// Convert the characters to indices
FT_UInt index1 = getGlyphIndex( first );
FT_UInt index2 = getGlyphIndex( second );
// Get the kerning vector
FT_Vector kerning;
kerning.x = kerning.y = 0;
if ( FT_HAS_KERNING( face ) )
FT_Get_Kerning( face, index1, index2, FT_KERNING_UNFITTED, &kerning );
// X advance is already in pixels for bitmap fonts
if ( !FT_IS_SCALABLE( face ) )
return static_cast<Float>( kerning.x );
if ( !FT_IS_SCALABLE( face ) ) {
kerningVal = static_cast<Float>( kerning.x );
} else {
auto firstRsbDelta = static_cast<Float>( glyph1.rsbDelta );
auto secondLsbDelta = static_cast<Float>( glyph2.lsbDelta );
kerningVal = std::floor(
( secondLsbDelta - firstRsbDelta + static_cast<float>( kerning.x ) + 32 ) /
static_cast<float>( 1 << 6 ) );
}
}
// Return the X advance
return std::floor(
( secondLsbDelta - firstRsbDelta + static_cast<float>( kerning.x ) + 32 ) /
static_cast<float>( 1 << 6 ) );
} else {
// Invalid font, or no kerning
return 0.f;
}
mKerningCache[key] = kerningVal;
return kerningVal;
}
Float FontTrueType::getKerningFromGlyphIndex( Uint32 index1, Uint32 index2,
@@ -847,6 +863,13 @@ Float FontTrueType::getKerningFromGlyphIndex( Uint32 index1, Uint32 index2,
if ( index1 == 0 || index2 == 0 || isMonospace() )
return 0.f;
Uint64 key = getKerningKey( index1, index2, characterSize, bold, italic, outlineThickness );
auto it = mKerningGlyphCache.find( key );
if ( it != mKerningGlyphCache.end() ) {
return it->second;
}
Float kerningVal = 0.f;
FT_Face face = static_cast<FT_Face>( mFace );
if ( face && setCurrentSize( characterSize ) ) {
@@ -864,18 +887,17 @@ Float FontTrueType::getKerningFromGlyphIndex( Uint32 index1, Uint32 index2,
// X advance is already in pixels for bitmap fonts
if ( !FT_IS_SCALABLE( face ) ) {
return static_cast<Float>( kerning.x );
kerningVal = static_cast<Float>( kerning.x );
} else {
// Get the X advance
kerningVal = std::floor(
( secondLsbDelta - firstRsbDelta + static_cast<float>( kerning.x ) + 32 ) /
static_cast<float>( 1 << 6 ) );
}
// Return the X advance
Float val =
std::floor( ( secondLsbDelta - firstRsbDelta + static_cast<float>( kerning.x ) + 32 ) /
static_cast<float>( 1 << 6 ) );
return val;
} else {
// Invalid font, or no kerning
return 0.f;
}
mKerningGlyphCache[key] = kerningVal;
return kerningVal;
}
Float FontTrueType::getLineSpacing( unsigned int characterSize ) const {
@@ -1062,6 +1084,8 @@ void FontTrueType::cleanup() {
mFontBoldItalicCb = 0;
mPages.clear();
std::vector<Uint8>().swap( mPixelBuffer );
mKerningCache.clear();
mKerningGlyphCache.clear();
mCodePointIndexCache.clear();
mKeyCache.clear();
mClosestCharacterSize.clear();
@@ -1768,6 +1792,8 @@ void FontTrueType::clearCache() {
mClosestCharacterSize.clear();
mCodePointIndexCache.clear();
mKeyCache.clear();
mKerningCache.clear();
mKerningGlyphCache.clear();
Text::GlobalInvalidationId++;
}

View File

@@ -784,13 +784,7 @@ Image::Image( const Uint8* imageData, const unsigned int& imageDataSize,
} else if ( webp_test_from_memory( imageData, imageDataSize ) ) {
webpLoad( imageData, imageDataSize );
} else {
std::string reason = ".";
if ( NULL != stbi_failure_reason() ) {
reason = ", reason: " + std::string( stbi_failure_reason() );
}
Log::error( "Failed to load image from memory. Reason: %s", reason.c_str() );
Log::error( "Failed to load image from memory. Reason: %s", stbi_failure_reason() );
}
}
@@ -1438,7 +1432,7 @@ Graphics::Image* Image::copy() {
}
Graphics::Image& Image::operator=( const Image& right ) {
if (this == &right)
if ( this == &right )
return *this;
mWidth = right.mWidth;

View File

@@ -57,7 +57,27 @@ void RichText::draw( const Float& X, const Float& Y, const Vector2f& scale, cons
std::visit(
Overloaded{
[&]( const std::shared_ptr<Text>& text ) {
[&]( const SpanBlock& spanBlock ) {
const std::shared_ptr<Text>& text = spanBlock.text;
Color oldBgColor = text->getFontStyleConfig().BackgroundColor;
if ( oldBgColor != Color::Transparent ) {
Primitives p;
p.setColor( oldBgColor );
Rectf bgRect(
Vector2f(
std::trunc( X + pos.x - spanBlock.padding.Left ),
std::trunc( Y + line.y + pos.y - spanBlock.padding.Top ) ),
Sizef( span.size.getWidth() + spanBlock.padding.Left +
spanBlock.padding.Right,
span.size.getHeight() + spanBlock.padding.Top +
spanBlock.padding.Bottom ) );
p.drawRectangle( bgRect, rotation, scale );
}
if ( oldBgColor != Color::Transparent )
text->setBackgroundColor( Color::Transparent );
bool selectionApplied = false;
if ( mSelectionColor != Color::Transparent ) {
TextSelectionRange spanSel = {
@@ -85,6 +105,9 @@ void RichText::draw( const Float& X, const Float& Y, const Vector2f& scale, cons
rotation, effect, rotationCenter, scaleCenter );
}
if ( oldBgColor != Color::Transparent )
text->setBackgroundColor( oldBgColor );
if ( selectionApplied )
text->invalidateColors();
},
@@ -127,9 +150,9 @@ Int64 RichText::findCharacterFromPos( const Vector2i& pos ) const {
if ( pos.y >= line.y && pos.y < line.y + line.height ) {
for ( const auto& span : line.spans ) {
if ( pos.x >= span.position.x && pos.x < span.position.x + span.size.getWidth() ) {
if ( auto pText = std::get_if<std::shared_ptr<Text>>( &span.block ) ) {
if ( auto pText = std::get_if<SpanBlock>( &span.block ) ) {
return span.startCharIndex +
( *pText )->findCharacterFromPos( Vector2i(
pText->text->findCharacterFromPos( Vector2i(
pos.x - span.position.x, pos.y - line.y - span.position.y ) );
} else {
return ( pos.x < span.position.x + span.size.getWidth() * 0.5f )
@@ -166,8 +189,8 @@ Vector2f RichText::findCharacterPos( Int64 index ) const {
for ( const auto& line : mLines ) {
for ( const auto& span : line.spans ) {
if ( index >= span.startCharIndex && index < span.endCharIndex ) {
if ( auto pText = std::get_if<std::shared_ptr<Text>>( &span.block ) ) {
Vector2f p = ( *pText )->findCharacterPos( index - span.startCharIndex );
if ( auto pText = std::get_if<SpanBlock>( &span.block ) ) {
Vector2f p = pText->text->findCharacterPos( index - span.startCharIndex );
return { span.position.x + p.x, line.y + span.position.y + p.y };
} else {
return { span.position.x, line.y + span.position.y };
@@ -197,8 +220,8 @@ SmallVector<Rectf> RichText::getSelectionRects() const {
Int64 spanEnd = std::min( end, span.endCharIndex );
if ( spanStart < spanEnd ) {
if ( auto pText = std::get_if<std::shared_ptr<Text>>( &span.block ) ) {
auto spanRects = ( *pText )->getSelectionRects(
if ( auto pText = std::get_if<SpanBlock>( &span.block ) ) {
auto spanRects = pText->text->getSelectionRects(
{ spanStart - span.startCharIndex, spanEnd - span.startCharIndex } );
for ( auto& rect : spanRects ) {
rect.move( { span.position.x, line.y + span.position.y } );
@@ -237,9 +260,9 @@ String RichText::getSelectionString() const {
Int64 spanEnd = std::min( end, span.endCharIndex );
if ( spanStart < spanEnd ) {
if ( auto pText = std::get_if<std::shared_ptr<Text>>( &span.block ) ) {
res += ( *pText )->getString().substr( spanStart - span.startCharIndex,
spanEnd - spanStart );
if ( auto pText = std::get_if<SpanBlock>( &span.block ) ) {
res += pText->text->getString().substr( spanStart - span.startCharIndex,
spanEnd - spanStart );
} else {
// It's a drawable or custom size, it takes 1 "character" index.
res += ' ';
@@ -264,14 +287,15 @@ Sizef RichText::getPixelsSize() {
return getSize();
}
void RichText::addSpan( const String& text, const FontStyleConfig& style ) {
if ( text.empty() )
void RichText::addSpan( const String& text, const FontStyleConfig& style, const Rectf& margin,
const Rectf& padding ) {
if ( text.empty() && margin == Rectf::Zero && padding == Rectf::Zero )
return;
auto span = std::make_shared<Text>();
span->setString( text );
span->setStyleConfig( style );
mBlocks.push_back( span ); // Implicitly constructs the variant's Text alternative
mBlocks.push_back( SpanBlock{ span, margin, padding } );
invalidateLayout();
}
@@ -282,11 +306,16 @@ void RichText::addDrawable( std::shared_ptr<Drawable> drawable ) {
invalidateLayout();
}
void RichText::addCustomSize( const Sizef& size, bool isBlock ) {
mBlocks.push_back( CustomBlock{ size, isBlock } );
void RichText::addCustomSize( const Sizef& size, bool isBlock, UI::CSSFloat floatType,
UI::CSSClear clearType ) {
mBlocks.push_back( CustomBlock{ size, isBlock, floatType, clearType } );
invalidateLayout();
}
void RichText::addSpan( const String& text, const FontStyleConfig& style ) {
addSpan( text, style, Rectf::Zero, Rectf::Zero );
}
void RichText::addSpan( const String& text, Font* font, Uint32 characterSize, Color color,
Uint32 style, Color backgroundColor ) {
FontStyleConfig config;
@@ -332,9 +361,9 @@ void RichText::setMaxWidth( Float width ) {
void RichText::invalidate() {
invalidateLayout();
for ( auto& block : mBlocks ) {
if ( auto pText = std::get_if<std::shared_ptr<Text>>( &block ) ) {
if ( *pText )
( *pText )->invalidate();
if ( auto pText = std::get_if<SpanBlock>( &block ) ) {
if ( pText->text )
pText->text->invalidate();
}
}
}
@@ -342,8 +371,8 @@ void RichText::invalidate() {
Float RichText::getMinIntrinsicWidth() {
Float minW = 0;
for ( auto& block : mBlocks ) {
if ( auto pText = std::get_if<std::shared_ptr<Text>>( &block ) ) {
auto& span = *pText;
if ( auto pText = std::get_if<SpanBlock>( &block ) ) {
auto& span = pText->text;
if ( !span || span->getString().empty() )
continue;
const String& s = span->getString();
@@ -359,7 +388,9 @@ Float RichText::getMinIntrinsicWidth() {
end++;
if ( start < end ) {
minW = std::max( minW, Text::getTextWidth( s.substr( start, end - start ),
span->getFontStyleConfig() ) );
span->getFontStyleConfig() ) +
pText->margin.Left + pText->margin.Right +
pText->padding.Left + pText->padding.Right );
}
start = end;
}
@@ -376,23 +407,25 @@ Float RichText::getMaxIntrinsicWidth() {
Float maxW = 0;
Float curX = 0;
for ( auto& block : mBlocks ) {
if ( auto pText = std::get_if<std::shared_ptr<Text>>( &block ) ) {
auto& span = *pText;
if ( auto pText = std::get_if<SpanBlock>( &block ) ) {
auto& span = pText->text;
if ( !span || span->getString().empty() )
continue;
const String& s = span->getString();
size_t start = 0;
size_t end = 0;
curX += pText->margin.Left + pText->padding.Left;
while ( ( end = s.find( '\n', start ) ) != String::InvalidPos ) {
curX += Text::getTextWidth( s.substr( start, end - start ),
span->getFontStyleConfig(), 4, span->getTextHints() );
maxW = std::max( maxW, curX );
maxW = std::max( maxW, curX + pText->margin.Right + pText->padding.Right );
curX = 0;
start = end + 1;
}
curX += Text::getTextWidth( s.substr( start ), span->getFontStyleConfig(), 4,
span->getTextHints() );
span->getTextHints() ) +
pText->margin.Right + pText->padding.Right;
} else if ( auto pDrawable = std::get_if<std::shared_ptr<Drawable>>( &block ) ) {
curX += ( *pDrawable )->getPixelsSize().getWidth();
} else if ( auto pSize = std::get_if<CustomBlock>( &block ) ) {
@@ -415,6 +448,241 @@ void RichText::updateLayout() {
if ( !mNeedsLayoutUpdate )
return;
// Detect whether any block has float/clear — if not, use the original
// non-float layout path which is simpler and faster.
bool hasFloats = false;
for ( auto& block : mBlocks ) {
if ( auto pSize = std::get_if<CustomBlock>( &block ) ) {
if ( pSize->floatType != UI::CSSFloat::None ||
pSize->clearType != UI::CSSClear::None ) {
hasFloats = true;
break;
}
}
}
// ─── Fast path: no floats or clears ─────────────────────────────
if ( !hasFloats ) {
mLines.clear();
mLines.push_back( RenderParagraph() );
Float curX = 0;
Float maxWidth = 0;
Int64 curCharIdx = 0;
// Pass 1: flow blocks into lines, wrapping at mMaxWidth.
for ( auto& block : mBlocks ) {
if ( auto pText = std::get_if<SpanBlock>( &block ) ) {
auto& span = pText->text;
if ( !span )
continue;
// Empty-string spans contribute only their margin/padding.
if ( span->getString().empty() ) {
Float l = pText->margin.Left + pText->padding.Left;
Float r = pText->margin.Right + pText->padding.Right;
if ( l <= 0 && r <= 0 )
continue;
curX += l + r;
if ( !mLines.empty() )
mLines.back().width += l + r;
continue;
}
auto& fontStyle = span->getFontStyleConfig();
if ( !fontStyle.Font )
continue;
Float extraLeft = pText->margin.Left + pText->padding.Left;
curX += extraLeft;
if ( !mLines.empty() )
mLines.back().width += extraLeft;
Uint32 textHints = span->getTextHints();
// Compute where lines break within this text span.
LineWrapInfoEx wrapInfo = LineWrap::computeLineBreaksEx(
span->getString(), fontStyle, mMaxWidth > 0 ? mMaxWidth : 1e9f,
mMaxWidth > 0 ? LineWrapMode::Word : LineWrapMode::NoWrap, false, 4, 0.f,
textHints, false, curX );
if ( wrapInfo.wraps.empty() ||
wrapInfo.wraps.back() != (Float)span->getString().size() )
wrapInfo.wraps.push_back( span->getString().size() );
// Emit a RenderSpan for each segment, wrapping to new lines as needed.
for ( size_t i = 0; i < wrapInfo.wraps.size() - 1; ++i ) {
size_t startIdx = wrapInfo.wraps[i];
size_t endIdx = wrapInfo.wraps[i + 1];
bool isNewline =
( endIdx - startIdx == 1 && span->getString()[startIdx] == '\n' );
if ( !isNewline ) {
std::shared_ptr<Text> renderSpanText = std::make_shared<Text>();
renderSpanText->setString(
span->getString().substr( startIdx, endIdx - startIdx ) );
renderSpanText->setStyleConfig( fontStyle );
Float ascent = fontStyle.Font->getAscent( fontStyle.CharacterSize );
Float height = fontStyle.Font->getLineSpacing( fontStyle.CharacterSize );
Float spanWidth = renderSpanText->getTextWidth();
RenderSpan renderSpan;
renderSpan.block =
SpanBlock{ renderSpanText, pText->margin, pText->padding };
renderSpan.position = { curX, 0 };
renderSpan.size = Sizef( spanWidth, height );
renderSpan.startCharIndex = curCharIdx;
renderSpan.endCharIndex = curCharIdx + ( endIdx - startIdx );
curCharIdx = renderSpan.endCharIndex;
RenderParagraph& currentLine = mLines.back();
currentLine.spans.push_back( renderSpan );
currentLine.maxAscent = std::max( currentLine.maxAscent, ascent );
currentLine.height = std::max( currentLine.height, height );
curX += spanWidth;
currentLine.width += spanWidth;
}
// After the last segment, add trailing margin and check if the
// margin itself forces a wrap.
if ( i == wrapInfo.wraps.size() - 2 && !isNewline ) {
Float extraRight = pText->margin.Right + pText->padding.Right;
curX += extraRight;
mLines.back().width += extraRight;
if ( !isNewline && mMaxWidth > 0 && curX > mMaxWidth ) {
maxWidth = std::max( maxWidth, curX );
mLines.push_back( RenderParagraph() );
curX = 0;
continue;
}
}
// Start a new line for hard breaks (newlines) or soft wraps.
if ( i < wrapInfo.wraps.size() - 2 || isNewline ) {
if ( isNewline ) {
curCharIdx++;
if ( i == wrapInfo.wraps.size() - 2 ) {
Float extraRight = pText->margin.Right + pText->padding.Right;
curX += extraRight;
mLines.back().width += extraRight;
}
}
maxWidth = std::max( maxWidth, curX );
mLines.push_back( RenderParagraph() );
curX = 0;
}
}
} else {
// Drawable or CustomBlock (non-float).
Sizef blockSize;
bool isBlock = false;
if ( auto pDrawable = std::get_if<std::shared_ptr<Drawable>>( &block ) ) {
auto& drawable = *pDrawable;
blockSize = drawable ? drawable->getPixelsSize() : Sizef();
} else if ( auto pSize = std::get_if<CustomBlock>( &block ) ) {
blockSize = pSize->size;
isBlock = pSize->isBlock;
}
// Block elements force a line break before themselves.
if ( isBlock && curX > 0 ) {
maxWidth = std::max( maxWidth, curX );
mLines.push_back( RenderParagraph() );
curX = 0;
}
// Inline elements that don't fit wrap to the next line.
if ( mMaxWidth > 0 && !isBlock &&
( curX + blockSize.getWidth() >= mMaxWidth || curX >= mMaxWidth ) &&
curX > 0 ) {
maxWidth = std::max( maxWidth, curX );
mLines.push_back( RenderParagraph() );
curX = 0;
}
RenderSpan renderSpan;
renderSpan.block = block;
renderSpan.position = { curX, 0 };
renderSpan.size = blockSize;
renderSpan.startCharIndex = curCharIdx;
renderSpan.endCharIndex = curCharIdx + 1;
curCharIdx = renderSpan.endCharIndex;
RenderParagraph& currentLine = mLines.back();
currentLine.spans.push_back( renderSpan );
currentLine.maxAscent = std::max( currentLine.maxAscent, blockSize.getHeight() );
currentLine.height = std::max( currentLine.height, blockSize.getHeight() );
curX += blockSize.getWidth();
currentLine.width += blockSize.getWidth();
// Block elements also force a line break after themselves.
if ( ( mMaxWidth > 0 && curX >= mMaxWidth ) || isBlock ) {
maxWidth = std::max( maxWidth, curX );
mLines.push_back( RenderParagraph() );
curX = 0;
}
}
}
maxWidth = std::max( maxWidth, curX );
// Remove trailing empty line if present.
if ( !mLines.empty() && mLines.back().spans.empty() && mLines.size() > 1 ) {
mLines.pop_back();
}
// Pass 2: assign Y positions to each line, apply text alignment,
// and compute vertical offsets for spans within their line.
Float curY = 0;
for ( auto& line : mLines ) {
line.y = curY;
// Compute horizontal alignment offset for this line.
Float xOffset = 0;
if ( mMaxWidth > 0 && mAlign != 0 ) {
Uint32 hAlign = Font::getHorizontalAlign( mAlign );
if ( hAlign == TEXT_ALIGN_CENTER ) {
xOffset = ( mMaxWidth - line.width ) * 0.5f;
} else if ( hAlign == TEXT_ALIGN_RIGHT ) {
xOffset = mMaxWidth - line.width;
}
}
Float maxLineHeight = 0;
for ( auto& span : line.spans ) {
if ( auto pText = std::get_if<SpanBlock>( &span.block ) ) {
auto& textBlock = pText->text;
Float offsetY = line.maxAscent - textBlock->getCharacterSize();
span.position.x += xOffset;
span.position.y = offsetY;
maxLineHeight = std::max( maxLineHeight, offsetY + span.size.getHeight() );
} else {
Float offsetY = line.maxAscent - span.size.getHeight();
if ( offsetY < 0 )
offsetY = 0;
span.position.x += xOffset;
span.position.y = offsetY;
maxLineHeight = std::max( maxLineHeight, offsetY + span.size.getHeight() );
}
}
line.height = std::max( line.height, maxLineHeight );
curY += line.height;
}
mSize = Sizef( maxWidth, curY );
mTotalCharacterCount = curCharIdx;
mNeedsLayoutUpdate = false;
return;
}
// ─── Float-aware path ────────────────────────────────────────────
mLines.clear();
mLines.push_back( RenderParagraph() );
@@ -422,24 +690,105 @@ void RichText::updateLayout() {
Float maxWidth = 0;
Int64 curCharIdx = 0;
// Active float rectangles: { left, top, right, bottom } in local coords.
std::vector<Rectf> leftFloats;
std::vector<Rectf> rightFloats;
Float curY = 0;
// ── Helper lambdas ─────────────────────────────────────────────
// Returns the rightmost x-coordinate occupied by left floats at the given y.
auto floatLeftEdge = [&]( Float y ) -> Float {
Float l = 0;
for ( auto& f : leftFloats ) {
if ( y >= f.Top && y < f.Bottom )
l = std::max( l, f.Right );
}
return l;
};
// Returns the leftmost x-coordinate occupied by right floats at the given y.
auto floatRightEdge = [&]( Float y ) -> Float {
Float r = mMaxWidth > 0 ? mMaxWidth : 1e9f;
for ( auto& f : rightFloats ) {
if ( y >= f.Top && y < f.Bottom )
r = std::min( r, f.Left );
}
return r;
};
// Available horizontal space at y, narrowed by active floats on both sides.
auto effectiveMaxWidthAt = [&]( Float y ) -> Float {
return floatRightEdge( y ) - floatLeftEdge( y );
};
// Advances curY past the bottom of active floats specified by clearType.
// Returns true if curY was moved.
auto clearFloats = [&]( UI::CSSClear clearType ) -> bool {
bool advanced = false;
if ( clearType == UI::CSSClear::Left || clearType == UI::CSSClear::Both ) {
for ( auto& f : leftFloats ) {
if ( f.Bottom > curY ) {
curY = f.Bottom;
advanced = true;
}
}
}
if ( clearType == UI::CSSClear::Right || clearType == UI::CSSClear::Both ) {
for ( auto& f : rightFloats ) {
if ( f.Bottom > curY ) {
curY = f.Bottom;
advanced = true;
}
}
}
return advanced;
};
// ── Pass 1: flow blocks with float awareness ────────────────────
for ( auto& block : mBlocks ) {
if ( auto pText = std::get_if<std::shared_ptr<Text>>( &block ) ) {
auto& span = *pText;
if ( !span || span->getString().empty() )
if ( auto pText = std::get_if<SpanBlock>( &block ) ) {
// ── Text span ─────────────────────────────────────────
auto& span = pText->text;
if ( !span )
continue;
if ( span->getString().empty() ) {
Float l = pText->margin.Left + pText->padding.Left;
Float r = pText->margin.Right + pText->padding.Right;
if ( l <= 0 && r <= 0 )
continue;
curX += l + r;
if ( !mLines.empty() )
mLines.back().width += l + r;
continue;
}
auto& fontStyle = span->getFontStyleConfig();
if ( !fontStyle.Font )
continue;
Float extraLeft = pText->margin.Left + pText->padding.Left;
curX += extraLeft;
if ( !mLines.empty() )
mLines.back().width += extraLeft;
// Shift curX inside to the left edge — text starts
// to the right of any left floats.
Float le = floatLeftEdge( curY );
if ( curX < le )
curX = le;
// Narrow the available width by active floats at this Y.
Uint32 textHints = span->getTextHints();
Float effW = effectiveMaxWidthAt( curY );
if ( mMaxWidth > 0 && mMaxWidth < effW )
effW = mMaxWidth;
LineWrapInfoEx wrapInfo = LineWrap::computeLineBreaksEx(
span->getString(), fontStyle, mMaxWidth > 0 ? mMaxWidth : 1e9f,
mMaxWidth > 0 ? LineWrapMode::Word : LineWrapMode::NoWrap, false, 4, 0.f, textHints,
false, curX );
LineWrapInfoEx wrapInfo =
LineWrap::computeLineBreaksEx( span->getString(), fontStyle, effW > 0 ? effW : 1e9f,
effW > 0 ? LineWrapMode::Word : LineWrapMode::NoWrap,
false, 4, 0.f, textHints, false, curX );
// Make sure we have the end of the string as a "wrap" point for the loop
if ( wrapInfo.wraps.empty() ||
wrapInfo.wraps.back() != (Float)span->getString().size() )
wrapInfo.wraps.push_back( span->getString().size() );
@@ -460,10 +809,9 @@ void RichText::updateLayout() {
Float spanWidth = renderSpanText->getTextWidth();
RenderSpan renderSpan;
renderSpan.block = renderSpanText;
renderSpan.position = { curX, 0 }; // Y adjusted later
renderSpan.size =
Sizef( spanWidth, height ); // Configured BEFORE pushing to vector
renderSpan.block = SpanBlock{ renderSpanText, pText->margin, pText->padding };
renderSpan.position = { curX, 0 };
renderSpan.size = Sizef( spanWidth, height );
renderSpan.startCharIndex = curCharIdx;
renderSpan.endCharIndex = curCharIdx + ( endIdx - startIdx );
curCharIdx = renderSpan.endCharIndex;
@@ -478,64 +826,141 @@ void RichText::updateLayout() {
currentLine.width += spanWidth;
}
// If it's a newline, or if it's not the very last segment (which means it wrapped),
// start a new line. Exception: If the last segment was just a newline, we already
// handled it.
// Trailing margin may force a wrap.
if ( i == wrapInfo.wraps.size() - 2 && !isNewline ) {
Float extraRight = pText->margin.Right + pText->padding.Right;
curX += extraRight;
mLines.back().width += extraRight;
if ( effW > 0 && effW < 1e9f && curX > effW ) {
maxWidth = std::max( maxWidth, curX );
mLines.push_back( RenderParagraph() );
curX = 0;
continue;
}
}
// Newline or soft-wrap → start a new line.
if ( i < wrapInfo.wraps.size() - 2 || isNewline ) {
if ( isNewline ) {
curCharIdx++;
if ( i == wrapInfo.wraps.size() - 2 ) {
Float extraRight = pText->margin.Right + pText->padding.Right;
curX += extraRight;
mLines.back().width += extraRight;
}
}
maxWidth = std::max( maxWidth, curX );
mLines.push_back( RenderParagraph() );
curX = 0;
}
}
} else { // Drawable or CustomSize
} else {
// ── Drawable or CustomBlock ────────────────────────────
Sizef blockSize;
bool isBlock = false;
UI::CSSFloat floatType = UI::CSSFloat::None;
UI::CSSClear clearType = UI::CSSClear::None;
if ( auto pDrawable = std::get_if<std::shared_ptr<Drawable>>( &block ) ) {
auto& drawable = *pDrawable;
blockSize = drawable ? drawable->getPixelsSize() : Sizef();
} else if ( auto pSize = std::get_if<CustomBlock>( &block ) ) {
blockSize = pSize->size;
isBlock = pSize->isBlock;
floatType = pSize->floatType;
clearType = pSize->clearType;
}
if ( isBlock && curX > 0 ) {
maxWidth = std::max( maxWidth, curX );
mLines.push_back( RenderParagraph() );
curX = 0;
// ── Clear: advance curY past active floats ─────────────
if ( clearType != UI::CSSClear::None ) {
if ( clearFloats( clearType ) ) {
maxWidth = std::max( maxWidth, curX );
mLines.push_back( RenderParagraph() );
curX = 0;
}
}
// Wrap if needed
if ( mMaxWidth > 0 && !isBlock &&
( curX + blockSize.getWidth() >= mMaxWidth || curX >= mMaxWidth ) && curX > 0 ) {
maxWidth = std::max( maxWidth, curX );
mLines.push_back( RenderParagraph() );
curX = 0;
}
// Left edge of open space at current Y (after any clears).
Float le = floatLeftEdge( curY );
RenderSpan renderSpan;
renderSpan.block = block;
renderSpan.position = { curX, 0 };
renderSpan.size = blockSize;
renderSpan.startCharIndex = curCharIdx;
renderSpan.endCharIndex = curCharIdx + 1;
curCharIdx = renderSpan.endCharIndex;
if ( floatType != UI::CSSFloat::None ) {
// ── Float placement ────────────────────────────────
// Position the float at the left/right edge of the
// available space. Floats do NOT consume inline-flow
// horizontal space (curX is not advanced) and are not
// affected by text-align (see pass 2).
Float posX;
if ( floatType == UI::CSSFloat::Left ) {
posX = le;
} else {
Float re = floatRightEdge( curY );
posX = re - blockSize.getWidth();
if ( posX < le )
posX = le;
}
RenderParagraph& currentLine = mLines.back();
currentLine.spans.push_back( renderSpan );
RenderSpan renderSpan;
renderSpan.block = block;
renderSpan.position = { posX, 0 };
renderSpan.size = blockSize;
renderSpan.startCharIndex = curCharIdx;
renderSpan.endCharIndex = curCharIdx + 1;
curCharIdx = renderSpan.endCharIndex;
currentLine.maxAscent = std::max( currentLine.maxAscent, blockSize.getHeight() );
currentLine.height = std::max( currentLine.height, blockSize.getHeight() );
mLines.back().spans.push_back( renderSpan );
curX += blockSize.getWidth();
currentLine.width += blockSize.getWidth();
// Record the float's bounding box so subsequent
// content can wrap around it.
Rectf fr( posX, curY, posX + blockSize.getWidth(),
curY + blockSize.getHeight() );
if ( floatType == UI::CSSFloat::Left )
leftFloats.push_back( fr );
else
rightFloats.push_back( fr );
} else {
// ── Normal (non-float) block ────────────────────
if ( curX < le )
curX = le;
if ( ( mMaxWidth > 0 && curX >= mMaxWidth ) || isBlock ) {
maxWidth = std::max( maxWidth, curX );
mLines.push_back( RenderParagraph() );
curX = 0;
// Block elements force a line break before.
if ( isBlock && curX > 0 ) {
maxWidth = std::max( maxWidth, curX );
mLines.push_back( RenderParagraph() );
curX = 0;
}
// Wrap if the block doesn't fit in the available width
// (narrowed by active floats).
Float effW = effectiveMaxWidthAt( curY );
if ( effW > 0 && effW < 1e9f && !isBlock &&
( curX + blockSize.getWidth() >= effW || curX >= effW ) && curX > 0 ) {
maxWidth = std::max( maxWidth, curX );
mLines.push_back( RenderParagraph() );
curX = 0;
}
RenderSpan renderSpan;
renderSpan.block = block;
renderSpan.position = { curX, 0 };
renderSpan.size = blockSize;
renderSpan.startCharIndex = curCharIdx;
renderSpan.endCharIndex = curCharIdx + 1;
curCharIdx = renderSpan.endCharIndex;
RenderParagraph& currentLine = mLines.back();
currentLine.spans.push_back( renderSpan );
currentLine.maxAscent = std::max( currentLine.maxAscent, blockSize.getHeight() );
currentLine.height = std::max( currentLine.height, blockSize.getHeight() );
curX += blockSize.getWidth();
currentLine.width += blockSize.getWidth();
// Block elements or overflow force a line break after.
if ( ( effW > 0 && effW < 1e9f && curX >= effW ) || isBlock ) {
maxWidth = std::max( maxWidth, curX );
mLines.push_back( RenderParagraph() );
curX = 0;
}
}
}
}
@@ -546,9 +971,12 @@ void RichText::updateLayout() {
mLines.pop_back();
}
Float curY = 0;
// ── Pass 2: assign Y positions and apply text alignment ───────
// NOTE: float spans are excluded from the xOffset because
// text-align only affects inline-flow content, not floated elements.
Float accumY = 0;
for ( auto& line : mLines ) {
line.y = curY;
line.y = accumY;
Float xOffset = 0;
if ( mMaxWidth > 0 && mAlign != 0 ) {
@@ -562,8 +990,13 @@ void RichText::updateLayout() {
Float maxLineHeight = 0;
for ( auto& span : line.spans ) {
if ( auto pText = std::get_if<std::shared_ptr<Text>>( &span.block ) ) {
auto& textBlock = *pText;
bool isFloat = false;
if ( auto pSize = std::get_if<CustomBlock>( &span.block ) ) {
if ( pSize->floatType != UI::CSSFloat::None )
isFloat = true;
}
if ( auto pText = std::get_if<SpanBlock>( &span.block ) ) {
auto& textBlock = pText->text;
Float offsetY = line.maxAscent - textBlock->getCharacterSize();
span.position.x += xOffset;
span.position.y = offsetY;
@@ -572,17 +1005,19 @@ void RichText::updateLayout() {
Float offsetY = line.maxAscent - span.size.getHeight();
if ( offsetY < 0 )
offsetY = 0;
span.position.x += xOffset;
// Float spans keep their edge-aligned x; only inline-flow spans shift.
if ( !isFloat )
span.position.x += xOffset;
span.position.y = offsetY;
maxLineHeight = std::max( maxLineHeight, offsetY + span.size.getHeight() );
}
}
line.height = std::max( line.height, maxLineHeight );
curY += line.height;
accumY += line.height;
}
mSize = Sizef( maxWidth, curY );
mSize = Sizef( maxWidth, accumY );
mTotalCharacterCount = curCharIdx;
mNeedsLayoutUpdate = false;
}

View File

@@ -2309,7 +2309,7 @@ Uint32 Text::getNumLines() {
return mString.countChar( '\n' ) + 1;
}
const std::vector<Float>& Text::getLinesWidth() {
const SmallVector<Float, 4>& Text::getLinesWidth() {
cacheWidth();
return mLinesWidth;

View File

@@ -538,8 +538,8 @@ TextLayout::Cache TextLayout::layout( const String& string, Font* font, const Ui
keepIndentation, initialXOffset );
}
std::vector<Float> TextLayout::getLinesWidth() const {
std::vector<Float> lw;
SmallVector<Float, 4> TextLayout::getLinesWidth() const {
SmallVector<Float, 4> lw;
std::size_t total = 0;
for ( const auto& sp : paragraphs )
total += sp.wrapInfo.wrapsWidth.size();
@@ -570,7 +570,7 @@ void TextLayout::wrapLayout( const String::View& string, TextLayout& result,
Sizef maxSize{ 0, vspace + yShift };
std::size_t startWrapsCount = sp.wrapInfo.wraps.size();
std::vector<Float> wrapsWidth = std::move( sp.wrapInfo.wrapsWidth );
auto wrapsWidth = std::move( sp.wrapInfo.wrapsWidth );
sp.wrapInfo.wrapsWidth.clear();
if ( keepIndentation && shapedGlyphCount ) {

View File

@@ -0,0 +1,79 @@
#include <eepp/network/cookiemanager.hpp>
#include <eepp/system/lock.hpp>
namespace EE { namespace Network {
CookieManager::CookieManager() {}
void CookieManager::storeCookies( const std::string& domain,
const Http::Response& response ) {
std::string setCookie = response.getField( "set-cookie" );
if ( !setCookie.empty() )
parseSetCookie( domain, setCookie );
}
void CookieManager::storeCookiesFromHeader( const std::string& domain,
const std::string& setCookieHeader ) {
if ( !setCookieHeader.empty() )
parseSetCookie( domain, setCookieHeader );
}
std::string CookieManager::getCookieHeader( const std::string& domain ) const {
Lock l( mMutex );
auto it = mCookies.find( domain );
if ( it == mCookies.end() || it->second.empty() )
return "";
std::string header;
for ( const auto& pair : it->second ) {
if ( !header.empty() )
header += "; ";
header += pair.first + "=" + pair.second;
}
return header;
}
void CookieManager::clear() {
Lock l( mMutex );
mCookies.clear();
}
size_t CookieManager::size() const {
Lock l( mMutex );
size_t total = 0;
for ( const auto& domainCookies : mCookies )
total += domainCookies.second.size();
return total;
}
bool CookieManager::empty() const {
Lock l( mMutex );
return mCookies.empty();
}
void CookieManager::parseSetCookie( const std::string& domain,
const std::string& setCookieHeader ) {
Lock l( mMutex );
size_t end = setCookieHeader.find( ';' );
std::string_view cookiePair( end != std::string::npos
? std::string_view( setCookieHeader ).substr( 0, end )
: std::string_view( setCookieHeader ) );
size_t eq = cookiePair.find( '=' );
if ( eq == std::string::npos )
return;
std::string name( cookiePair.substr( 0, eq ) );
std::string value( cookiePair.substr( eq + 1 ) );
if ( name.empty() )
return;
mCookies[domain][String::trim( name )] = String::trim( value );
}
bool CookieManager::hasCookie( const std::string& domain ) const {
return mCookies.find( domain ) != mCookies.end();
}
}} // namespace EE::Network

View File

@@ -48,6 +48,8 @@ std::string Http::Request::statusToString( Http::Request::Status status ) {
return "HeaderReceived";
case ContentReceived:
return "ContentReceived";
case Redirect:
return "Redirect";
}
return "";
}
@@ -449,6 +451,10 @@ Http::Response Http::Response::createFakeResponse( const Http::Response::FieldTa
Http::Response::Response() : mStatus( ConnectionFailed ), mMajorVersion( 0 ), mMinorVersion( 0 ) {}
const Http::Response::FieldTable& Http::Response::getHeaders() const {
return mFields;
}
Http::Response::FieldTable Http::Response::getHeaders() {
return mFields;
}
@@ -653,11 +659,13 @@ Uint64 Http::requestAsync( const Http::AsyncResponseCallback& cb, const URI& uri
const Time& timeout, Request::Method method,
const Http::Request::ProgressCallback& progressCallback,
const Http::Request::FieldTable& headers, const std::string& body,
const bool& validateCertificate, const URI& proxy ) {
const bool& validateCertificate, const URI& proxy,
bool followRedirect ) {
auto http = sGlobalHttpPool.get( uri, proxy );
Request request( uri.getPathAndQuery(), method, body, validateCertificate, validateCertificate,
true, true );
request.setProgressCallback( progressCallback );
request.setFollowRedirect( followRedirect );
for ( const auto& field : headers )
request.setField( field.first, field.second );
@@ -668,17 +676,17 @@ Uint64 Http::requestAsync( const Http::AsyncResponseCallback& cb, const URI& uri
Uint64 Http::getAsync( const Http::AsyncResponseCallback& cb, const URI& uri, const Time& timeout,
const Http::Request::ProgressCallback& progressCallback,
const Http::Request::FieldTable& headers, const std::string& body,
const bool& validateCertificate, const URI& proxy ) {
const bool& validateCertificate, const URI& proxy, bool followRedirect ) {
return requestAsync( cb, uri, timeout, Request::Method::Get, progressCallback, headers, body,
validateCertificate, proxy );
validateCertificate, proxy, followRedirect );
}
Uint64 Http::postAsync( const Http::AsyncResponseCallback& cb, const URI& uri, const Time& timeout,
const Http::Request::ProgressCallback& progressCallback,
const Http::Request::FieldTable& headers, const std::string& body,
const bool& validateCertificate, const URI& proxy ) {
const bool& validateCertificate, const URI& proxy, bool followRedirect ) {
return requestAsync( cb, uri, timeout, Request::Method::Post, progressCallback, headers, body,
validateCertificate, proxy );
validateCertificate, proxy, followRedirect );
}
Http::Http() : mConnection( NULL ), mHost(), mPort( 0 ), mIsSSL( false ), mHostSolved( false ) {}
@@ -1091,25 +1099,45 @@ Http::Response Http::downloadRequest( const Http::Request& request, IOStream& wr
eeSAFE_DELETE( chunkedStream );
eeSAFE_DELETE( inflateStream );
Http::Request newRequest( request );
newRequest.setUri( uri.getPathAndQuery() );
request.mRedirectionCount++;
newRequest.mRedirectionCount =
request.mRedirectionCount;
// Same host, expects a path in the same domain
if ( uri.getHost().empty() ||
uri.getHost() == getHostName() ) {
return downloadRequest( newRequest, writeTo,
timeout );
if ( !request.isCancelled() &&
!sendProgress( *this, request, received,
Request::Redirect, contentLength,
currentTotalBytes ) ) {
request.mCancel = true;
} else {
// New host, we need to solve the host
Http http( uri.getHost(), uri.getPort(),
uri.getScheme() == "https" ? true
: false );
return http.downloadRequest( newRequest, writeTo,
timeout );
Http::Request newRequest( request );
newRequest.setUri( uri.getPathAndQuery() );
newRequest.setMethod(
Http::Request::getRedirectMethodFromStatus(
request.getMethod(),
received.getStatus() ) );
newRequest.setProgressCallback(
request.getProgressCallback() );
if ( received.hasField( "set-cookie" ) ) {
newRequest.setField(
"Cookie",
received.getField( "set-cookie" ) );
}
request.mRedirectionCount++;
newRequest.mRedirectionCount =
request.mRedirectionCount;
// Same host, expects a path in the same domain
if ( uri.getHost().empty() ||
uri.getHost() == getHostName() ) {
return downloadRequest( newRequest, writeTo,
timeout );
} else {
// New host, we need to solve the host
Http http( uri.getHost(), uri.getPort(),
uri.getScheme() == "https" ? true
: false );
return http.downloadRequest( newRequest,
writeTo, timeout );
}
}
}
}
@@ -1195,6 +1223,26 @@ Http::Response Http::downloadRequest( const Http::Request& request, IOStream& wr
return received;
}
Http::Request::Method
Http::Request::getRedirectMethodFromStatus( Method requestMethod,
Response::Status responseStatus ) {
// 1. 307 and 308 ALWAYS preserve the original method.
if ( responseStatus == Http::Response::PermanentRedirect ||
responseStatus == Http::Response::TemporaryRedirect ) {
return requestMethod;
}
// 2. 303 See Other ALWAYS converts to GET (unless it
// was a HEAD).
if ( responseStatus == Http::Response::SeeOther ) {
return requestMethod == Http::Request::Method::Head ? Http::Request::Method::Head
: Http::Request::Method::Get;
}
// 3. 301 and 302 historically convert POST to GET, but
// preserve others.
return requestMethod == Http::Request::Method::Post ? Http::Request::Method::Get
: requestMethod;
}
void Http::endConnection() {
if ( mConnection && !mConnection->isKeepAlive() ) {
if ( mConnection->isConnected() )

View File

@@ -775,6 +775,18 @@ bool Node::isClosing() const {
return 0 != ( mNodeFlags & NODE_FLAG_CLOSE );
}
bool Node::inClosingTree() const {
if ( isClosing() )
return true;
Node* parent = mParentNode;
while ( parent != nullptr ) {
if ( parent->isClosing() )
return true;
parent = parent->mParentNode;
}
return false;
}
bool Node::isClosingChildren() const {
return 0 != ( mNodeFlags & NODE_FLAG_CLOSING_CHILDREN );
}
@@ -784,7 +796,7 @@ const String::HashType& Node::getIdHash() const {
}
Node* Node::findIdHash( const String::HashType& idHash ) const {
if ( !isClosing() && mIdHash == idHash ) {
if ( !isClosing() && mIdHash == idHash && !inClosingTree() ) {
return const_cast<Node*>( this );
} else {
Node* child = mChild;
@@ -821,7 +833,7 @@ Node* Node::hasChild( const std::string& id ) const {
}
Node* Node::findByType( const Uint32& type ) const {
if ( !isClosing() && isType( type ) ) {
if ( !isClosing() && isType( type ) && !inClosingTree() ) {
return const_cast<Node*>( this );
} else {
Node* child = mChild;
@@ -839,7 +851,7 @@ Node* Node::findByType( const Uint32& type ) const {
std::vector<Node*> Node::findAllByType( const Uint32& type ) const {
std::vector<Node*> nodes;
if ( !isClosing() && isType( type ) )
if ( !isClosing() && isType( type ) && !inClosingTree() )
nodes.push_back( const_cast<Node*>( this ) );
Node* child = mChild;
@@ -1064,6 +1076,10 @@ bool Node::isWidget() const {
return 0 != ( mNodeFlags & NODE_FLAG_WIDGET );
}
bool Node::isTextNode() const {
return 0 != ( mNodeFlags & NODE_FLAG_TEXTNODE );
}
bool Node::isWindow() const {
return 0 != ( mNodeFlags & NODE_FLAG_WINDOW );
}

View File

@@ -7,7 +7,7 @@ namespace EE { namespace System {
/* $Id: base64.c 156 2007-07-12 23:29:10Z orange $ */
/* decode a base64 string in one shot */
int Base64::decode( size_t in_len, const char* in, size_t out_len, unsigned char* out ) {
size_t Base64::decode( size_t in_len, const char* in, size_t out_len, unsigned char* out ) {
static const Uint8 base64dec_tab[256] = {
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
@@ -26,17 +26,18 @@ int Base64::decode( size_t in_len, const char* in, size_t out_len, unsigned char
255, 255, 255, 255,
};
unsigned ii, io;
size_t ii, io;
Uint32 v;
unsigned rem;
for ( io = 0, ii = 0, v = 0, rem = 0; ii < in_len; ii++ ) {
unsigned char ch;
if ( isspace( in[ii] ) )
unsigned char c = (unsigned char)in[ii];
if ( isspace( c ) )
continue;
if ( in[ii] == '=' )
if ( c == '=' )
break; /* stop at = */
ch = base64dec_tab[(unsigned)in[ii]];
ch = base64dec_tab[c];
if ( ch == 255 )
break; /* stop at a parse error */
v = ( v << 6 ) | ch;
@@ -57,11 +58,11 @@ int Base64::decode( size_t in_len, const char* in, size_t out_len, unsigned char
return io;
}
int Base64::encode( size_t in_len, const unsigned char* in, size_t out_len, char* out ) {
size_t Base64::encode( size_t in_len, const unsigned char* in, size_t out_len, char* out ) {
static const Uint8 base64enc_tab[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
unsigned ii, io;
size_t ii, io;
Uint32 v;
unsigned rem;
@@ -94,14 +95,14 @@ int Base64::encode( size_t in_len, const unsigned char* in, size_t out_len, char
return io;
}
bool Base64::encode( const std::string& in, std::string& out ) {
bool Base64::encode( std::string_view in, std::string& out ) {
size_t b64len = encodeSafeOutLen( in.size() );
if ( out.size() < b64len ) {
out.resize( b64len );
}
int len = encode( in.size(), (const unsigned char*)in.c_str(), out.size(), (char*)&out[0] );
int len = encode( in.size(), (const unsigned char*)in.data(), out.size(), (char*)&out[0] );
if ( -1 != len && (size_t)len != out.size() ) {
out.resize( len );
@@ -110,20 +111,20 @@ bool Base64::encode( const std::string& in, std::string& out ) {
return -1 != len;
}
bool Base64::decode( const std::string& in, std::string& out ) {
size_t Base64::decode( std::string_view in, std::string& out ) {
size_t d64len = decodeSafeOutLen( in.size() );
if ( out.size() < d64len ) {
out.resize( d64len );
}
int len = decode( in.size(), in.c_str(), out.size(), (unsigned char*)&out[0] );
int len = decode( in.size(), in.data(), out.size(), (unsigned char*)&out[0] );
if ( -1 != len && (size_t)len != out.size() ) {
out.resize( len );
}
return -1 != len;
return len;
}
}} // namespace EE::System

View File

@@ -0,0 +1,273 @@
#include <eepp/graphics/richtext.hpp>
#include <eepp/ui/blocklayouter.hpp>
#include <eepp/ui/uihtmlwidget.hpp>
#include <eepp/ui/uirichtext.hpp>
#include <eepp/ui/uistyle.hpp>
#include <eepp/ui/uitextnode.hpp>
#include <eepp/ui/uitextspan.hpp>
namespace EE { namespace UI {
Float BlockLayouter::getMinIntrinsicWidth() {
computeIntrinsicWidths();
return mMinIntrinsicWidth;
}
Float BlockLayouter::getMaxIntrinsicWidth() {
computeIntrinsicWidths();
return mMaxIntrinsicWidth;
}
void BlockLayouter::computeIntrinsicWidths() {
if ( !mContainer->isType( UI_TYPE_HTML_WIDGET ) )
return;
auto* widget = mContainer->asType<UIHTMLWidget>();
auto* rt = widget->getRichTextPtr();
if ( rt == nullptr )
return;
if ( mContainer->getLayoutWidthPolicy() == SizePolicy::Fixed ) {
// Do nothing here, UIWidget handles fixed width.
return;
}
if ( mIntrinsicWidthsDirty ) {
RichText tmpRt( *rt );
UIRichText::rebuildRichText( widget, tmpRt, UIRichText::IntrinsicMode::Min );
mMinIntrinsicWidth = tmpRt.getMinIntrinsicWidth() +
mContainer->getPixelsContentOffset().Left +
mContainer->getPixelsContentOffset().Right;
UIRichText::rebuildRichText( widget, tmpRt, UIRichText::IntrinsicMode::Max );
mMaxIntrinsicWidth = tmpRt.getMaxIntrinsicWidth() +
mContainer->getPixelsContentOffset().Left +
mContainer->getPixelsContentOffset().Right;
mIntrinsicWidthsDirty = false;
}
}
void BlockLayouter::updateLayout() {
if ( !mContainer->isType( UI_TYPE_HTML_WIDGET ) )
return;
auto* widget = mContainer->asType<UIHTMLWidget>();
auto* rt = widget->getRichTextPtr();
if ( rt == nullptr || mPacking )
return;
mResizedCount = 0;
mPacking = true;
mContainer->beginAttributesTransaction();
setMatchParentIfNeededVerticalGrowth();
const StyleSheetProperty* prop = nullptr;
if ( mContainer->getLayoutWidthPolicy() == SizePolicy::Fixed && mContainer->getUIStyle() &&
( prop = mContainer->getUIStyle()->getProperty( PropertyId::Width ) ) ) {
mContainer->setInternalPixelsSize(
{ mContainer->lengthFromValue( *prop ), mContainer->getPixelsSize().getHeight() } );
}
UIRichText::rebuildRichText( widget, *rt );
rt->updateLayout();
positionRichTextChildren( rt );
Float totW = mContainer->getPixelsSize().getWidth();
if ( mContainer->getLayoutWidthPolicy() == SizePolicy::WrapContent ) {
totW = rt->getSize().getWidth() + mContainer->getPixelsContentOffset().Left +
mContainer->getPixelsContentOffset().Right;
if ( !mContainer->getMaxWidthEq().empty() && totW > mContainer->getMaxSizePx().getWidth() )
mContainer->setClipType( ClipType::ContentBox );
}
if ( totW != mContainer->getPixelsSize().getWidth() ||
mContainer->getLayoutWidthPolicy() == SizePolicy::WrapContent )
mContainer->setInternalPixelsWidth( totW );
Float totH = mContainer->getPixelsSize().getHeight();
if ( mContainer->getLayoutHeightPolicy() == SizePolicy::WrapContent ) {
totH = rt->getSize().getHeight() + mContainer->getPixelsContentOffset().Top +
mContainer->getPixelsContentOffset().Bottom;
if ( !mContainer->getMaxHeightEq().empty() &&
totH > mContainer->getMaxSizePx().getHeight() )
mContainer->setClipType( ClipType::ContentBox );
}
if ( totH != mContainer->getPixelsSize().getHeight() ||
mContainer->getLayoutHeightPolicy() == SizePolicy::WrapContent )
mContainer->setInternalPixelsHeight( totH );
mContainer->endAttributesTransaction();
if ( mResizedCount > 0 )
positionRichTextChildren( rt );
mPacking = false;
mResizedCount = 0;
}
void BlockLayouter::positionRichTextChildren( Graphics::RichText* rt ) {
const auto& lines = rt->getLines();
Node* child = mContainer->getFirstChild();
size_t currentLine = 0;
size_t currentSpan = 0;
auto getNextCustomSpan = [&]() -> const RichText::RenderSpan* {
while ( currentLine < lines.size() ) {
const auto& line = lines[currentLine];
while ( currentSpan < line.spans.size() ) {
const auto& span = line.spans[currentSpan];
currentSpan++;
if ( std::holds_alternative<RichText::CustomBlock>( span.block ) )
return &span;
}
currentSpan = 0;
currentLine++;
}
return nullptr;
};
Int64 curCharIdx = 0;
auto processNode = [&]( Node* node, auto& processNodeRef ) -> Rectf {
constexpr Float maxF = std::numeric_limits<Float>::max();
constexpr Float lowF = std::numeric_limits<Float>::lowest();
Rectf bounds( maxF, maxF, lowF, lowF );
if ( !node->isVisible() )
return bounds;
// UITextNode is a logical marker; its text is rendered by the
// RichText engine — just advance the character index and return
// empty bounds so it does not affect any widget's geometry.
if ( node->isTextNode() ) {
curCharIdx += static_cast<UITextNode*>( node )->getText().length();
return bounds;
}
if ( !node->isWidget() )
return bounds;
UIWidget* widget = node->asType<UIWidget>();
// Accumulate ancestor positions so the widget can be placed
// relative to the container (mContainer).
Vector2f offset;
Node* p = widget->getParent();
while ( p && p != mContainer ) {
offset += p->isWidget() ? p->asType<UIWidget>()->getPixelsPosition() : p->getPosition();
p = p->getParent();
}
if ( widget->isType( UI_TYPE_HTML_WIDGET ) &&
widget->asType<UIHTMLWidget>()->isMergeable() ) {
UITextSpan* textSpan = widget->asType<UITextSpan>();
Int64 startChar = curCharIdx;
Int64 endChar = curCharIdx;
if ( !textSpan->getText().empty() ) {
endChar += textSpan->getText().length();
curCharIdx = endChar;
}
auto& hitBoxes = textSpan->getHitBoxes();
hitBoxes.clear();
if ( startChar < endChar ) {
for ( const auto& line : lines ) {
bool passedText = false;
for ( const auto& rspan : line.spans ) {
if ( rspan.startCharIndex >= startChar && rspan.endCharIndex <= endChar ) {
Rectf hb( mContainer->getPixelsContentOffset().Left + rspan.position.x,
mContainer->getPixelsContentOffset().Top + line.y +
rspan.position.y,
mContainer->getPixelsContentOffset().Left + rspan.position.x +
rspan.size.getWidth(),
mContainer->getPixelsContentOffset().Top + line.y +
rspan.position.y + rspan.size.getHeight() );
hitBoxes.push_back( hb );
bounds.expand( hb );
} else if ( rspan.startCharIndex > endChar ) {
passedText = true;
break;
}
}
if ( passedText )
break;
}
}
// Recurse into children. UITextNode children advance
// curCharIdx but contribute no geometry (they are logical
// markers only). Widget children get their own position
// and hit-boxes.
Node* spanChild = widget->getFirstChild();
while ( spanChild != NULL ) {
if ( spanChild->isWidget() )
bounds.expand( processNodeRef( spanChild, processNodeRef ) );
spanChild = spanChild->getNextNode();
}
if ( bounds.Left <= bounds.Right && bounds.Top <= bounds.Bottom ) {
Vector2f boundsPos = bounds.getPosition();
widget->setPixelsPosition( boundsPos - offset );
if ( bounds.getSize() != widget->getPixelsSize() ) {
widget->setPixelsSize( bounds.getSize() );
mResizedCount++;
}
for ( auto& hb : hitBoxes )
hb.move( -boundsPos );
} else {
hitBoxes.clear();
}
} else if ( widget->isType( UI_TYPE_BR ) ) {
curCharIdx += 1;
Vector2f pos;
if ( widget->getPrevNode() && widget->getPrevNode()->isWidget() ) {
pos = widget->getPrevNode()->asType<UIWidget>()->getPixelsPosition();
pos.y += widget->getPrevNode()->getPixelsSize().getHeight();
}
widget->setPixelsPosition( pos );
widget->setPixelsSize( { eemax( 0.f, mContainer->getPixelsSize().getWidth() -
mContainer->getPixelsContentOffset().Left -
mContainer->getPixelsContentOffset().Right ),
0 } );
} else {
curCharIdx += 1;
const auto* span = getNextCustomSpan();
if ( span ) {
size_t lineIdx = currentSpan > 0 ? currentLine : currentLine - 1;
Float lineY = lines[lineIdx].y;
Rectf margin = widget->getLayoutPixelsMargin();
Vector2f targetPos( mContainer->getPixelsContentOffset().Left + span->position.x +
margin.Left,
mContainer->getPixelsContentOffset().Top + lineY +
span->position.y + margin.Top );
widget->setPixelsPosition( targetPos - offset );
bounds = Rectf( targetPos, span->size );
}
}
return bounds;
};
child = mContainer->getFirstChild();
while ( NULL != child ) {
bool isOutOfFlow =
child->isType( UI_TYPE_HTML_WIDGET ) && child->asType<UIHTMLWidget>()->isOutOfFlow();
if ( !isOutOfFlow )
processNode( child, processNode );
child = child->getNextNode();
}
}
}} // namespace EE::UI

View File

@@ -149,14 +149,120 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve
borderRight = eemin( (int)( size.getWidth() * 0.5f ), (int)borders.right.width );
}
// draw top border
if ( borderTop ) {
double leftW = eemin( halfWidth, eemax( 0.f, borders.radius.topLeft.x ) );
double rightW = eemin( halfHeight, eemax( 0.f, borders.radius.topRight.x ) );
double leftH = eemin( halfWidth, eemax( 0.f, borders.radius.topLeft.y ) );
double rightH = eemin( halfHeight, eemax( 0.f, borders.radius.topRight.y ) );
bool hasTop = borderTop > 0;
bool hasRight = borderRight > 0;
bool hasBottom = borderBottom > 0;
bool hasLeft = borderLeft > 0;
if ( leftW ) {
if ( !hasTop && !hasRight && !hasBottom && !hasLeft )
return;
// Pre-compute arc radii for each corner
double tlArcW = eemin( halfWidth, eemax( 0.f, borders.radius.topLeft.x ) );
double tlArcH = eemin( halfWidth, eemax( 0.f, borders.radius.topLeft.y ) );
double trArcW = eemin( halfHeight, eemax( 0.f, borders.radius.topRight.x ) );
double trArcH = eemin( halfHeight, eemax( 0.f, borders.radius.topRight.y ) );
double brArcW = eemin( halfHeight, eemax( 0.f, borders.radius.bottomRight.x ) );
double brArcH = eemin( halfHeight, eemax( 0.f, borders.radius.bottomRight.y ) );
double blArcW = eemin( halfWidth, eemax( 0.f, borders.radius.bottomLeft.x ) );
double blArcH = eemin( halfWidth, eemax( 0.f, borders.radius.bottomLeft.y ) );
// Corner positions
Vector2f tlInner( pos.x + borderLeft, pos.y + borderTop );
Vector2f tlOuter( pos.x, pos.y );
Vector2f trInner( pos.x + size.getWidth() - borderRight, pos.y + borderTop );
Vector2f trOuter( pos.x + size.getWidth(), pos.y );
Vector2f brInner( pos.x + size.getWidth() - borderRight,
pos.y + size.getHeight() - borderBottom );
Vector2f brOuter( pos.x + size.getWidth(), pos.y + size.getHeight() );
Vector2f blInner( pos.x + borderLeft, pos.y + size.getHeight() - borderBottom );
Vector2f blOuter( pos.x, pos.y + size.getHeight() );
// Helper: compute arc outer vertex at a given angle
auto arcOuterPos = []( const Vector2f& center, double rW, double rH,
double angleDeg ) -> Vector2f {
return Vector2f( center.x + rW * Math::cosAng( angleDeg ),
center.y + rH * Math::sinAng( angleDeg ) );
};
// Helper: compute arc inner vertex at a given angle
auto arcInnerPos = []( const Vector2f& center, double rW, double rH, double angleDeg,
double lineW, const Vector2f& basePos ) -> Vector2f {
if ( rW > lineW )
return Vector2f( center.x + ( rW - lineW ) * Math::cosAng( angleDeg ),
center.y + ( rH - lineW ) * Math::sinAng( angleDeg ) );
return basePos;
};
// Pre-compute first inner vertex of each border (used as bridge targets)
// Top border first inner (top-left corner)
Vector2f topFirstInner;
if ( tlArcW > 0 && hasLeft ) {
Vector2f tlCenter( pos.x + tlArcW, pos.y + tlArcH );
topFirstInner =
arcInnerPos( tlCenter, tlArcW, tlArcH, 225, borderTop,
Vector2f( pos.x + borderLeft, pos.y + borderTop ) );
} else {
topFirstInner = tlInner;
}
// Right border first inner (top-right corner)
Vector2f rightFirstInner;
if ( trArcW > 0 && hasTop ) {
Vector2f trCenter( pos.x + size.getWidth() - trArcW, pos.y + trArcH );
rightFirstInner =
arcInnerPos( trCenter, trArcW, trArcH, 315, borderRight,
Vector2f( pos.x + size.getWidth() - borderRight, pos.y + borderTop ) );
} else {
rightFirstInner = trInner;
}
// Bottom border first inner (bottom-right corner)
Vector2f bottomFirstInner;
if ( brArcW > 0 && hasRight ) {
Vector2f brCenter( pos.x + size.getWidth() - brArcW,
pos.y + size.getHeight() - brArcH );
bottomFirstInner =
arcInnerPos( brCenter, brArcW, brArcH, 45, borderBottom,
Vector2f( pos.x + size.getWidth() - borderRight,
pos.y + size.getHeight() - borderBottom ) );
} else {
bottomFirstInner = brInner;
}
// Left border first inner (bottom-left corner)
Vector2f leftFirstInner;
if ( blArcW > 0 && hasBottom ) {
Vector2f blCenter( pos.x + blArcW, pos.y + size.getHeight() - blArcH );
leftFirstInner =
arcInnerPos( blCenter, blArcW, blArcH, 135, borderLeft,
Vector2f( pos.x + borderLeft,
pos.y + size.getHeight() - borderBottom ) );
} else {
leftFirstInner = blInner;
}
// Helper: insert degenerate triangle bridge between two disconnected border sections
auto addBridge = [&]( const Vector2f& fromOuter, const Vector2f& toInner,
const Color& bridgeColor ) {
vbo->addVertex( fromOuter );
vbo->addColor( bridgeColor );
vbo->addVertex( toInner );
vbo->addColor( bridgeColor );
vbo->addVertex( toInner );
vbo->addColor( bridgeColor );
};
Vector2f lastOuter; // last emitted outer vertex, used as bridge source
// --- draw top border ---
if ( hasTop ) {
double leftW = tlArcW;
double rightW = trArcW;
double leftH = tlArcH;
double rightH = trArcH;
if ( leftW && hasLeft ) {
double endAngle = 270;
double startAngle = 225;
@@ -170,7 +276,7 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve
vbo->addColor( borders.top.color );
}
if ( rightW ) {
if ( rightW && hasRight ) {
double startAngle = 270;
double endAngle = 315;
Vector2f basePos( pos.x + size.getWidth() - borderRight, pos.y + borderTop );
@@ -191,22 +297,33 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve
borderAddArc( vbo, tPos, rightW, rightH, startAngle, endAngle, borders.top.color,
borderTop, basePos );
lastOuter = arcOuterPos( tPos, rightW, rightH, endAngle );
} else {
vbo->addVertex( Vector2f( pos.x + size.getWidth() - borderRight, pos.y + borderTop ) );
vbo->addColor( borders.top.color );
vbo->addVertex( Vector2f( pos.x + size.getWidth(), pos.y ) );
vbo->addColor( borders.top.color );
lastOuter = trOuter;
}
if ( !hasRight ) {
if ( hasBottom )
addBridge( lastOuter, bottomFirstInner, borders.top.color );
else if ( hasLeft )
addBridge( lastOuter, leftFirstInner, borders.top.color );
}
}
// draw right border
if ( borderRight ) {
double topW = eemin( halfWidth, eemax( 0.f, borders.radius.topRight.x ) );
double bottomW = eemin( halfHeight, eemax( 0.f, borders.radius.bottomRight.x ) );
double topH = eemin( halfWidth, eemax( 0.f, borders.radius.topRight.y ) );
double bottomH = eemin( halfHeight, eemax( 0.f, borders.radius.bottomRight.y ) );
// --- draw right border ---
if ( hasRight ) {
double topW = trArcW;
double bottomW = brArcW;
double topH = trArcH;
double bottomH = brArcH;
if ( topW ) {
if ( topW && hasTop ) {
double startAngle = 315;
double endAngle = 360;
Vector2f basePos( pos.x + size.getWidth() - borderRight, pos.y + borderTop );
@@ -220,7 +337,7 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve
vbo->addColor( borders.right.color );
}
if ( bottomH ) {
if ( bottomH && hasBottom ) {
double startAngle = 0;
double endAngle = 45;
Vector2f basePos( pos.x + size.getWidth() - borderRight,
@@ -243,23 +360,29 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve
borderAddArc( vbo, tPos, bottomW, bottomH, startAngle, endAngle, borders.right.color,
borderRight, basePos );
lastOuter = arcOuterPos( tPos, bottomW, bottomH, endAngle );
} else {
vbo->addVertex( Vector2f( pos.x + size.getWidth() - borderRight,
pos.y + size.getHeight() - borderBottom ) );
vbo->addColor( borders.right.color );
vbo->addVertex( Vector2f( pos.x + size.getWidth(), pos.y + size.getHeight() ) );
vbo->addColor( borders.right.color );
lastOuter = brOuter;
}
if ( !hasBottom && hasLeft )
addBridge( lastOuter, leftFirstInner, borders.right.color );
}
// draw bottom border
if ( borderBottom ) {
double leftW = eemin( halfWidth, eemax( 0.f, borders.radius.bottomLeft.x ) );
double rightW = eemin( halfHeight, eemax( 0.f, borders.radius.bottomRight.x ) );
double leftH = eemin( halfWidth, eemax( 0.f, borders.radius.bottomLeft.y ) );
double rightH = eemin( halfHeight, eemax( 0.f, borders.radius.bottomRight.y ) );
// --- draw bottom border ---
if ( hasBottom ) {
double leftW = blArcW;
double rightW = brArcW;
double leftH = blArcH;
double rightH = brArcH;
if ( rightW ) {
if ( rightW && hasRight ) {
double startAngle = 45;
double endAngle = 90;
Vector2f basePos( pos.x + size.getWidth() - borderRight,
@@ -277,7 +400,7 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve
vbo->addColor( borders.bottom.color );
}
if ( leftW ) {
if ( leftW && hasLeft ) {
double startAngle = 90;
double endAngle = 135;
Vector2f basePos( pos.x + borderLeft, pos.y + size.getHeight() - borderBottom );
@@ -299,23 +422,30 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve
borderAddArc( vbo, tPos, leftW, leftH, startAngle, endAngle, borders.bottom.color,
borderBottom, basePos );
lastOuter = arcOuterPos( tPos, leftW, leftH, endAngle );
} else {
vbo->addVertex(
Vector2f( pos.x + borderLeft, pos.y + size.getHeight() - borderBottom ) );
vbo->addColor( borders.bottom.color );
vbo->addVertex( Vector2f( pos.x, pos.y + size.getHeight() ) );
vbo->addColor( borders.bottom.color );
lastOuter = blOuter;
}
// After bottom, only left remains (already checked or skipped).
// Bottom and left are adjacent, no bridge needed.
}
// draw left border
if ( borderLeft ) {
double topW = eemin( halfWidth, eemax( 0.f, borders.radius.topLeft.x ) );
double bottomW = eemin( halfHeight, eemax( 0.f, borders.radius.bottomLeft.x ) );
double topH = eemin( halfWidth, eemax( 0.f, borders.radius.topLeft.y ) );
double bottomH = eemin( halfHeight, eemax( 0.f, borders.radius.bottomLeft.y ) );
// --- draw left border ---
if ( hasLeft ) {
double topW = tlArcW;
double bottomW = blArcW;
double topH = tlArcH;
double bottomH = blArcH;
if ( bottomW ) {
if ( bottomW && hasBottom ) {
double startAngle = 135;
double endAngle = 180;
Vector2f basePos( pos.x + borderLeft, pos.y + size.getHeight() - borderBottom );
@@ -331,7 +461,7 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve
vbo->addColor( borders.left.color );
}
if ( topW ) {
if ( topW && hasTop ) {
double startAngle = 180;
double endAngle = 225;
Vector2f basePos( pos.x + borderLeft, pos.y + borderTop );

View File

@@ -7,6 +7,7 @@
#include <eepp/graphics/triangledrawable.hpp>
#include <eepp/scene/scenemanager.hpp>
#include <eepp/system/log.hpp>
#include <eepp/system/luapattern.hpp>
#include <eepp/ui/css/drawableimageparser.hpp>
#include <eepp/ui/uiiconthememanager.hpp>
#include <eepp/ui/uinode.hpp>
@@ -331,12 +332,14 @@ void DrawableImageParser::registerBaseParsers() {
UINode* node ) -> Drawable* {
if ( functionType.getParameters().size() < 1 )
return NULL;
return DrawableSearcher::searchByName(
node->getUISceneNode()
->solveRelativePath( functionType.getParameters().at( 0 ) )
.toString(),
false, node->getUISceneNode()->getReferer() );
const auto& param = functionType.getParameters().at( 0 );
if ( functionType.getName() == "url" && !param.empty() && param[0] != '@' &&
!String::startsWith( param, "data:image/" ) ) {
return DrawableSearcher::searchByName(
node->getUISceneNode()->solveRelativePath( param ).toString(), false,
node->getUISceneNode()->getReferer() );
}
return DrawableSearcher::searchByName( param, false, node->getUISceneNode()->getReferer() );
};
mFuncs["icon"] = []( const FunctionString& functionType, const Sizef& size, bool&,

View File

@@ -41,8 +41,9 @@ KeyframesDefinition::getPropertyDefinitionList() const {
std::map<PropertyId, const PropertyDefinition*> propDefs;
for ( auto& block : keyframeBlocks ) {
for ( auto& property : block.second.properties ) {
propDefs[property.second.getPropertyDefinition()->getPropertyId()] =
property.second.getPropertyDefinition();
auto propDef = property.second.getPropertyDefinition();
if ( propDef )
propDefs[propDef->getPropertyId()] = property.second.getPropertyDefinition();
}
}
return propDefs;

View File

@@ -21,7 +21,7 @@ ShorthandDefinition::ShorthandDefinition( const std::string& name,
mFuncName( shorthandParserName ),
mId( String::hash( name ) ),
mProperties( properties ) {
for ( auto& sep : {"-", "_"} ) {
for ( auto& sep : { "-", "_" } ) {
if ( mName.find( sep ) != std::string::npos ) {
std::string alias( name );
String::replaceAll( alias, sep, "" );

View File

@@ -80,16 +80,21 @@ int StyleSheetPropertiesParser::readPropertyValue( StyleSheetPropertiesParser::R
mPrevRs = rs;
bool inString = false;
bool inDoubleQuote = false;
bool inSingleQuote = false;
int nestedParenthesis = 0;
int prevChar = -1;
while ( pos < str.size() ) {
if ( str[pos] == '/' && str.size() > pos + 1 && str[pos + 1] == '*' ) {
// Ensure we aren't parsing comments inside strings
if ( str[pos] == '/' && str.size() > pos + 1 && str[pos + 1] == '*' && !inDoubleQuote &&
!inSingleQuote ) {
rs = ReadingComment;
return pos;
}
if ( str[pos] == ';' && !inString ) {
// Only terminate property parsing on ';' if we are outside of quotes and parentheses
if ( str[pos] == ';' && !inDoubleQuote && !inSingleQuote && nestedParenthesis == 0 ) {
rs = ReadingPropertyName;
addProperty( propName, buffer );
@@ -97,8 +102,16 @@ int StyleSheetPropertiesParser::readPropertyValue( StyleSheetPropertiesParser::R
return pos + 1;
}
if ( str[pos] == '"' && prevChar != '\\' )
inString = !inString;
// Keep track of quotes and nested parentheses
if ( str[pos] == '"' && prevChar != '\\' && !inSingleQuote ) {
inDoubleQuote = !inDoubleQuote;
} else if ( str[pos] == '\'' && prevChar != '\\' && !inDoubleQuote ) {
inSingleQuote = !inSingleQuote;
} else if ( str[pos] == '(' && !inDoubleQuote && !inSingleQuote ) {
nestedParenthesis++;
} else if ( str[pos] == ')' && !inDoubleQuote && !inSingleQuote && nestedParenthesis > 0 ) {
nestedParenthesis--;
}
if ( str[pos] != '\n' && str[pos] != '\r' && str[pos] != '\t' )
buffer += str[pos];
@@ -147,13 +160,13 @@ void StyleSheetPropertiesParser::addProperty( std::string name, std::string valu
StyleSheetSpecification::instance()->getShorthand( name )->parse( value );
for ( auto& property : properties )
mProperties.emplace( std::make_pair( property.getId(), std::move( property ) ) );
mProperties[property.getId()] = std::move( property );
} else {
if ( String::startsWith( name, "--" ) ) {
mVariables[String::hash( name )] = StyleSheetVariable( name, value );
} else {
StyleSheetProperty property( name, value );
mProperties.emplace( std::make_pair( property.getId(), std::move( property ) ) );
mProperties[property.getId()] = std::move( property );
}
}
}

View File

@@ -171,6 +171,11 @@ void StyleSheetSpecification::registerDefaultProperties() {
registerProperty( "layout-to-top-of", "" ).addAlias( "layout_to_top_of" );
registerProperty( "layout-to-bottom-of", "" ).addAlias( "layout_to_bottom_of" );
registerProperty( "clip", "" ).setType( PropertyType::String );
// TODO: layer implement overflow-x and overflow-y properly
registerProperty( "overflow", "visible" )
.addAlias( "overflow-x" )
.addAlias( "overflow-y" )
.setType( PropertyType::String );
registerProperty( "rotation", "" ).addAlias( "rotate" ).setType( PropertyType::NumberFloat );
registerProperty( "scale", "" ).setType( PropertyType::Vector2 );
registerProperty( "rotation-origin-point-x", "50%" )
@@ -425,6 +430,28 @@ void StyleSheetSpecification::registerDefaultProperties() {
registerProperty( "cols", "20" ).setType( PropertyType::NumberInt );
registerProperty( "input-mode", "normal" ).setType( PropertyType::String );
registerProperty( "hidden", "" ).setType( PropertyType::Bool );
registerProperty( "display", "inline" ).setType( PropertyType::String );
registerProperty( "position", "static" ).setType( PropertyType::String );
registerProperty( "float", "none" ).setType( PropertyType::String );
registerProperty( "clear", "none" ).setType( PropertyType::String );
registerProperty( "list-style-type", "none", true ).setType( PropertyType::String );
registerProperty( "list-style-position", "outside", true ).setType( PropertyType::String );
registerProperty( "list-style-image", "none" ).setType( PropertyType::String );
registerProperty( "top", "auto" )
.setType( PropertyType::NumberLength )
.setRelativeTarget( PropertyRelativeTarget::ContainingBlockHeight );
registerProperty( "right", "auto" )
.setType( PropertyType::NumberLength )
.setRelativeTarget( PropertyRelativeTarget::ContainingBlockWidth );
registerProperty( "bottom", "auto" )
.setType( PropertyType::NumberLength )
.setRelativeTarget( PropertyRelativeTarget::ContainingBlockHeight );
registerProperty( "left", "auto" )
.setType( PropertyType::NumberLength )
.setRelativeTarget( PropertyRelativeTarget::ContainingBlockWidth );
registerProperty( "z-index", "auto" ).setType( PropertyType::NumberInt );
registerProperty( "inner-widget-orientation", "widgeticontextbox" )
.setType( PropertyType::String );
@@ -447,6 +474,14 @@ void StyleSheetSpecification::registerDefaultProperties() {
registerProperty( "display-options", "" ).setType( PropertyType::String );
registerProperty( "menu-width-mode", "" ).setType( PropertyType::String );
registerProperty( "data-language", "" ).setType( PropertyType::String );
registerProperty( "action", "" ).setType( PropertyType::String );
registerProperty( "method", "GET" ).setType( PropertyType::String );
registerProperty( "enctype", "application/x-www-form-urlencoded" )
.setType( PropertyType::String );
registerProperty( "target", "_self" ).setType( PropertyType::String );
// Shorthands
registerShorthand( "margin", { "margin-top", "margin-right", "margin-bottom", "margin-left" },
"box" );
@@ -505,6 +540,11 @@ void StyleSheetSpecification::registerDefaultProperties() {
registerShorthand( "border-bottom",
{ "border-bottom-width", "border-bottom-style", "border-bottom-color" },
"border-side" );
registerShorthand( "list-style",
{ "list-style-type", "list-style-position", "list-style-image" },
"list-style" );
registerShorthand( "font", { "font-style", "font-size", "line-spacing", "font-family" },
"font" );
}
void StyleSheetSpecification::registerNodeSelector( const std::string& name,
@@ -526,7 +566,7 @@ void StyleSheetSpecification::registerDefaultNodeSelectors() {
};
mNodeSelectors["first-child"] = []( const UIWidget* node, int, int,
const FunctionString& ) -> bool {
return NULL != node->getParent() && node->getParent()->getFirstChild() == node;
return NULL != node->getParent() && node->getElementIndex() == 0;
};
mNodeSelectors["enabled"] = []( const UIWidget* node, int, int,
const FunctionString& ) -> bool { return node->isEnabled(); };
@@ -534,69 +574,69 @@ void StyleSheetSpecification::registerDefaultNodeSelectors() {
const FunctionString& ) -> bool { return !node->isEnabled(); };
mNodeSelectors["first-of-type"] = []( const UIWidget* node, int, int,
const FunctionString& ) -> bool {
Node* child = NULL != node->getParent() ? node->getParent()->getFirstChild() : NULL;
Uint32 type = node->getType();
while ( NULL != child ) {
if ( type == child->getType() ) {
return child == node;
}
child = child->getNextNode();
};
return false;
return NULL != node->getParent() && node->getElementOfTypeIndex() == 0;
};
mNodeSelectors["last-child"] = []( const UIWidget* node, int, int,
const FunctionString& ) -> bool {
return NULL != node->getParent() && node->getParent()->getLastChild() == node;
if ( NULL == node->getParent() || !node->getParent()->isWidget() )
return false;
Node* child = node->getParent()->getLastChild();
while ( NULL != child ) {
if ( child->isWidget() && !static_cast<UIWidget*>( child )->isTextNode() )
return child == node;
child = child->getPrevNode();
}
return false;
};
mNodeSelectors["last-of-type"] = []( const UIWidget* node, int, int,
const FunctionString& ) -> bool {
Node* child = NULL != node->getParent() ? node->getParent()->getLastChild() : NULL;
if ( NULL == node->getParent() || !node->getParent()->isWidget() )
return false;
Uint32 type = node->getType();
Node* child = node->getParent()->getLastChild();
while ( NULL != child ) {
if ( type == child->getType() ) {
if ( child->getType() == type && child->isWidget() &&
!static_cast<UIWidget*>( child )->isTextNode() )
return child == node;
}
child = child->getPrevNode();
};
}
return false;
};
mNodeSelectors["only-child"] = []( const UIWidget* node, int, int,
const FunctionString& ) -> bool {
return NULL != node->getParent() && node->getParent()->getChildCount() == 1;
return NULL != node->getParent() && node->getParent()->isWidget() &&
static_cast<const UIWidget*>( node->getParent() )->getChildElementCount() == 1;
};
mNodeSelectors["only-of-type"] = []( const UIWidget* node, int, int,
const FunctionString& ) -> bool {
Node* child = NULL != node->getParent() ? node->getParent()->getFirstChild() : NULL;
Uint32 type = node->getType();
Uint32 typeCount = 0;
while ( NULL != child ) {
if ( child->getType() == type ) {
typeCount++;
}
if ( typeCount > 1 )
return false;
child = child->getNextNode();
};
return typeCount == 1;
return NULL != node->getParent() && node->getParent()->isWidget() &&
static_cast<const UIWidget*>( node->getParent() )
->getChildElementOfTypeCount( node->getType() ) == 1;
};
mNodeSelectors["nth-child"] = []( const UIWidget* node, int a, int b,
const FunctionString& ) -> bool {
return isNth( a, b, node->getNodeIndex() + 1 );
return isNth( a, b, node->getElementIndex() + 1 );
};
mNodeSelectors["nth-last-child"] = []( const UIWidget* node, int a, int b,
const FunctionString& ) -> bool {
return isNth( a, b, node->getChildCount() - node->getNodeIndex() );
return node->getParent() != NULL && node->getParent()->isWidget()
? isNth(
a, b,
static_cast<const UIWidget*>( node->getParent() )->getChildElementCount() -
node->getElementIndex() )
: false;
};
mNodeSelectors["nth-of-type"] = []( const UIWidget* node, int a, int b,
const FunctionString& ) -> bool {
return isNth( a, b, node->getNodeOfTypeIndex() + 1 );
return isNth( a, b, node->getElementOfTypeIndex() + 1 );
};
mNodeSelectors["nth-last-of-type"] = []( const UIWidget* node, int a, int b,
const FunctionString& ) -> bool {
return node->getParent() != NULL
return node->getParent() != NULL && node->getParent()->isWidget()
? isNth( a, b,
node->getParent()->getChildOfTypeCount( node->getType() ) -
node->getNodeOfTypeIndex() )
static_cast<const UIWidget*>( node->getParent() )
->getChildElementOfTypeCount( node->getType() ) -
node->getElementOfTypeIndex() )
: false;
};
mNodeSelectors["checked"] = []( const UIWidget* node, int, int,
@@ -969,7 +1009,10 @@ void StyleSheetSpecification::registerDefaultShorthandParsers() {
std::string positionStr;
for ( auto& tok : tokens ) {
if ( mDrawableImageParser.exists( tok ) ) {
auto open = tok.find_first_of( '(' );
if ( open != std::string::npos &&
mDrawableImageParser.exists( tok.substr( 0, open ) ) ) {
int pos = getIndexEndingWith( propNames, "-image" );
if ( pos != -1 )
properties.emplace_back( StyleSheetProperty( propNames[pos], tok ) );
@@ -1124,6 +1167,186 @@ void StyleSheetSpecification::registerDefaultShorthandParsers() {
return properties;
};
mShorthandParsers["list-style"] = []( const ShorthandDefinition* shorthand,
std::string value ) -> std::vector<StyleSheetProperty> {
value = String::trim( value );
if ( value.empty() )
return {};
std::vector<StyleSheetProperty> properties;
const std::vector<std::string>& propNames = shorthand->getProperties();
if ( propNames.empty() )
return {};
auto tokens = String::split( value, " ", "", "(" );
int typePos = getIndexEndingWith( propNames, "-type" );
int posPos = getIndexEndingWith( propNames, "-position" );
int imagePos = getIndexEndingWith( propNames, "-image" );
for ( auto& tok : tokens ) {
String::trimInPlace( tok );
if ( tok == "inside" || tok == "outside" ) {
if ( posPos != -1 )
properties.emplace_back( StyleSheetProperty( propNames[posPos], tok ) );
} else if ( String::startsWith( tok, "url(" ) ) {
if ( imagePos != -1 )
properties.emplace_back( StyleSheetProperty( propNames[imagePos], tok ) );
} else if ( tok == "none" ) {
if ( typePos != -1 )
properties.emplace_back( StyleSheetProperty( propNames[typePos], tok ) );
if ( imagePos != -1 )
properties.emplace_back( StyleSheetProperty( propNames[imagePos], tok ) );
} else {
if ( typePos != -1 )
properties.emplace_back( StyleSheetProperty( propNames[typePos], tok ) );
}
}
return properties;
};
mShorthandParsers["font"] = []( const ShorthandDefinition* shorthand,
std::string value ) -> std::vector<StyleSheetProperty> {
value = String::trim( value );
if ( value.empty() )
return {};
std::string lowerVal = String::toLower( value );
static const std::string systemFonts[] = { "caption", "icon", "menu",
"message-box", "small-caption", "status-bar" };
for ( const auto& sysFont : systemFonts ) {
if ( lowerVal == sysFont )
return {};
}
std::vector<StyleSheetProperty> properties;
const std::vector<std::string>& propNames = shorthand->getProperties();
int stylePos = getIndexEndingWith( propNames, "-style" );
int sizePos = getIndexEndingWith( propNames, "-size" );
int linePos = getIndexEndingWith( propNames, "-spacing" );
int familyPos = getIndexEndingWith( propNames, "-family" );
static const std::string sizeKeywords[] = {
"xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large" };
auto isSizeKeyword = []( const std::string& t ) {
std::string lt = String::toLower( t );
for ( const auto& kw : sizeKeywords ) {
if ( lt == kw )
return true;
}
return false;
};
auto isStyleWord = []( const std::string& t ) {
std::string lt = String::toLower( t );
return lt == "italic" || lt == "oblique" || lt == "normal";
};
auto isWeightWord = []( const std::string& t ) {
std::string lt = String::toLower( t );
return lt == "bold" || lt == "bolder" || lt == "lighter" || lt == "100" ||
lt == "200" || lt == "300" || lt == "400" || lt == "500" || lt == "600" ||
lt == "700" || lt == "800" || lt == "900";
};
auto isNumberOrLength = []( const std::string& t ) {
if ( t.empty() )
return false;
return ( t[0] >= '0' && t[0] <= '9' ) || t[0] == '.' || t[0] == '-';
};
std::vector<std::string> tokens = String::split( value, " ", "", "(", "\"" );
std::string styleStr;
std::string sizeStr;
std::string lineStr;
std::string familyStr;
bool inLineHeight = false;
for ( size_t i = 0; i < tokens.size(); i++ ) {
std::string tok = tokens[i];
String::trimInPlace( tok );
if ( tok.empty() )
continue;
if ( tok == "/" ) {
inLineHeight = true;
continue;
}
if ( !inLineHeight ) {
size_t slashPos = tok.find( '/' );
if ( slashPos != std::string::npos ) {
if ( slashPos == 0 ) {
lineStr = tok.substr( 1 );
String::trimInPlace( lineStr );
continue;
}
sizeStr = tok.substr( 0, slashPos );
lineStr = tok.substr( slashPos + 1 );
String::trimInPlace( lineStr );
continue;
}
}
if ( inLineHeight ) {
lineStr += ( lineStr.empty() ? "" : " " ) + tok;
inLineHeight = false;
continue;
}
if ( !sizeStr.empty() && familyStr.empty() && !isStyleWord( tok ) &&
!isWeightWord( tok ) ) {
familyStr += ( familyStr.empty() ? "" : " " ) + tok;
continue;
}
if ( isStyleWord( tok ) ) {
std::string lt = String::toLower( tok );
if ( lt != "normal" ) {
if ( !styleStr.empty() )
styleStr += "|";
styleStr += lt;
}
continue;
}
if ( isWeightWord( tok ) ) {
std::string lt = String::toLower( tok );
if ( lt != "normal" ) {
if ( !styleStr.empty() )
styleStr += "|";
styleStr += "bold";
}
continue;
}
if ( sizeStr.empty() && ( isNumberOrLength( tok ) || isSizeKeyword( tok ) ) ) {
sizeStr = tok;
continue;
}
familyStr += ( familyStr.empty() ? "" : " " ) + tok;
}
if ( !sizeStr.empty() ) {
if ( stylePos != -1 && !styleStr.empty() )
properties.emplace_back( StyleSheetProperty( propNames[stylePos], styleStr ) );
if ( sizePos != -1 )
properties.emplace_back( StyleSheetProperty( propNames[sizePos], sizeStr ) );
if ( linePos != -1 && !lineStr.empty() )
properties.emplace_back( StyleSheetProperty( propNames[linePos], lineStr ) );
if ( familyPos != -1 && !familyStr.empty() ) {
String::trimInPlace( familyStr );
if ( familyStr.size() >= 2 &&
( ( familyStr[0] == '"' && familyStr.back() == '"' ) ||
( familyStr[0] == '\'' && familyStr.back() == '\'' ) ) ) {
familyStr = familyStr.substr( 1, familyStr.size() - 2 );
}
properties.emplace_back( StyleSheetProperty( propNames[familyPos], familyStr ) );
}
}
return properties;
};
}
}}} // namespace EE::UI::CSS

View File

@@ -37,7 +37,7 @@ namespace EE { namespace UI { namespace Doc {
// "minimap_highlight" (Minimap text highlight color)
// "minimap_visible_area" (Minimap visible area marker color)
SyntaxColorScheme SyntaxColorScheme::getDefault() {
SyntaxColorScheme SyntaxColorScheme::getDefaultDark() {
return {
"eepp",
{
@@ -86,6 +86,55 @@ SyntaxColorScheme SyntaxColorScheme::getDefault() {
{ "minimap_visible_area"_sst, Color( "#FFFFFF0A" ) } } };
}
SyntaxColorScheme SyntaxColorScheme::getDefaultLight() {
return {
"github",
{
{ "normal"_sst, Color( "#24292e" ) },
{ "symbol"_sst, Color( "#24292e" ) },
{ "comment"_sst, Color( "#6a737d" ) },
{ "keyword"_sst, Color( "#d73a49" ) },
{ "type"_sst, Color( "#d73a49" ) },
{ "parameter"_sst, Color( "#005cc5" ) },
{ "number"_sst, Color( "#005cc5" ) },
{ "literal"_sst, Color( "#005cc5" ) },
{ "string"_sst, Color( "#032f62" ) },
{ "operator"_sst, Color( "#d73a49" ) },
{ "function"_sst, Color( "#005cc5" ) },
{ "link"_sst, Color( "#0366d6" ) }, // Using 'accent' for link color
{ "link_hover"_sst, { Color::Transparent, Color::Transparent, Text::Underlined } },
},
{ { "background"_sst, Color( "#fbfbfb" ) },
{ "widget_background"_sst, Color( "#f6f6f6" ) },
{ "text"_sst, Color( "#404040" ) },
{ "caret"_sst, Color( "#181818" ) },
{ "selection"_sst, Color( "#b7dce8" ) },
{ "line_highlight"_sst, Color( "#f2f2f2" ) },
{ "line_number"_sst, Color( "#d0d0d0" ) },
{ "line_number2"_sst, Color( "#808080" ) },
// eepp colors
{ "gutter_background"_sst, Color( "#fbfbfb" ) },
{ "whitespace"_sst, Color( "#b7dce8" ) },
{ "line_break_column"_sst, Color( "#d0d0d099" ) },
{ "matching_bracket"_sst, Color( "#00000033" ) }, // Dark transparent for light theme
{ "matching_selection"_sst, Color( "#a1c8d6" ) },
{ "matching_search"_sst, Color( "#e8e8e8" ) },
{ "suggestion"_sst, { Color( "#1d1f27" ), Color( "#e1e1e6" ), Text::Regular } },
{ "suggestion_scrollbar"_sst, { Color( "#3daee9" ) } },
{ "suggestion_selected"_sst, { Color( "#222533" ), Color( "#ffffff" ), Text::Regular } },
{ "error"_sst, { Color( "#990000FF" ) } },
{ "warning"_sst, { Color( "#999900FF" ) } },
{ "notice"_sst, Color( "#8abdff" ) },
{ "selection_region"_sst, Color( "#b7dce877" ) },
// minimap colors
{ "minimap_background"_sst, Color( "#fbfbfbAA" ) },
{ "minimap_current_line"_sst, Color( "#0000000A" ) },
{ "minimap_hover"_sst, Color( "#00000010" ) },
{ "minimap_selection"_sst, Color( "#b7dce880" ) },
{ "minimap_highlight"_sst, Color( "#FF00FFFF" ) },
{ "minimap_visible_area"_sst, Color( "#00000011" ) } } };
}
SyntaxColorScheme::Style parseStyle(
const std::string& value, bool* colorWasSet = nullptr,
const UnorderedMap<SyntaxStyleType, SyntaxColorScheme::Style>* syntaxColors = nullptr ) {
@@ -151,7 +200,7 @@ SyntaxColorScheme::Style parseStyle(
std::vector<SyntaxColorScheme> SyntaxColorScheme::loadFromStream( IOStream& stream ) {
Clock clock;
std::vector<SyntaxColorScheme> colorSchemes;
SyntaxColorScheme refColorScheme( getDefault() );
SyntaxColorScheme refColorScheme( getDefaultDark() );
IniFile ini( stream );
for ( size_t keyIdx = 0; keyIdx < ini.getNumKeys(); keyIdx++ ) {
SyntaxColorScheme colorScheme;
@@ -216,7 +265,7 @@ SyntaxColorScheme::SyntaxColorScheme( const std::string& name,
mName( name ), mSyntaxColors( syntaxColors ), mEditorColors( editorColors ) {}
static const SyntaxColorScheme::Style StyleEmpty = { Color::Transparent };
static const SyntaxColorScheme StyleDefault = SyntaxColorScheme::getDefault();
static const SyntaxColorScheme StyleDefault = SyntaxColorScheme::getDefaultDark();
const SyntaxColorScheme::Style&
SyntaxColorScheme::getSyntaxStyle( const SyntaxStyleType& type ) const {

View File

@@ -1819,9 +1819,13 @@ TextPosition TextDocument::positionOffset( TextPosition position, int columnOffs
// As long as we are not at a grapheme boundary yet, keep
// moving the cursor position until we arrive at one
while ( cursorIndex > 0 && cursorIndex < lineLen - 1 &&
!getLineText( position.line() ).isGraphemeBoundary( cursorIndex ) ) {
cursorIndex += direction;
if ( position.line() >= 0 && position.line() < static_cast<Int64>( mLines.size() ) ) {
Lock l( mLinesMutex );
const auto& text = mLines[position.line()].getText();
while ( cursorIndex > 0 && cursorIndex < lineLen - 1 &&
!text.isGraphemeBoundary( cursorIndex ) ) {
cursorIndex += direction;
}
}
if ( position.column() != cursorIndex )
@@ -3883,10 +3887,11 @@ static inline void changeDepth( SyntaxHighlighter* highlighter, int& depth, cons
TextPosition TextDocument::getMatchingBracket( TextPosition sp,
const String::StringBaseType& openBracket,
const String::StringBaseType& closeBracket,
MatchDirection dir, bool allowDepth ) {
MatchDirection dir, bool allowDepth, Time timeout ) {
SyntaxHighlighter* highlighter = getHighlighter();
int depth = 0;
while ( sp.isValid() ) {
Clock c;
while ( sp.isValid() && ( timeout == Time::Zero || c.getElapsedTime() < timeout ) ) {
auto byte = getCharFromUnsanitizedPosition( sp );
if ( byte == openBracket ) {
changeDepth( highlighter, depth, sp, 1 );

View File

@@ -0,0 +1,535 @@
#include <eepp/ui/tablelayouter.hpp>
#include <eepp/ui/uihtmltable.hpp>
#include <eepp/ui/uistyle.hpp>
namespace EE { namespace UI {
static inline Float sanitizeFloat( Float val ) {
return std::isfinite( val ) ? val : 0.f;
}
void TableLayouter::setTableLayout( TableLayout layout ) {
if ( layout != mTableLayout ) {
mTableLayout = layout;
}
}
TableLayout TableLayouter::getTableLayout() const {
return mTableLayout;
}
void TableLayouter::setCellPadding( Float padding ) {
mCellpadding = padding;
}
Float TableLayouter::getCellPadding() const {
return mCellpadding;
}
void TableLayouter::setCellSpacing( Float spacing ) {
mCellspacing = spacing;
}
Float TableLayouter::getCellSpacing() const {
return mCellspacing;
}
Float TableLayouter::getMinIntrinsicWidth() {
computeIntrinsicWidths();
return mMinIntrinsicWidth;
}
Float TableLayouter::getMaxIntrinsicWidth() {
computeIntrinsicWidths();
return mMaxIntrinsicWidth;
}
void TableLayouter::computeIntrinsicWidths() {
if ( !mIntrinsicWidthsDirty )
return;
mRows.clear();
mHead = nullptr;
mBody = nullptr;
mFooter = nullptr;
mCells.clear();
mRowCellOffsets.clear();
auto collectRows = [&]( auto&& self, Node* node ) -> void {
for ( Node* child = node->getFirstChild(); child; child = child->getNextNode() ) {
if ( child->getType() == UI_TYPE_HTML_TABLE_ROW ) {
mRows.push_back( child->asType<UIHTMLTableRow>() );
} else if ( child->getType() != UI_TYPE_HTML_TABLE ) {
if ( child->getType() == UI_TYPE_HTML_TABLE_HEAD )
mHead = child->asType<UIHTMLTableHead>();
else if ( child->getType() == UI_TYPE_HTML_TABLE_BODY )
mBody = child->asType<UIHTMLTableBody>();
else if ( child->getType() == UI_TYPE_HTML_TABLE_FOOTER )
mFooter = child->asType<UIHTMLTableFooter>();
self( self, child );
}
}
};
collectRows( collectRows, mContainer );
auto getRecursiveSpecifiedWidth = [&]( auto&& self, Node* node ) -> Float {
if ( !node->isWidget() )
return 0.f;
if ( node->isType( UI_TYPE_HTML_WIDGET ) && node->asType<UIHTMLWidget>()->isOutOfFlow() )
return 0.f;
UIWidget* widget = node->asType<UIWidget>();
Float spec = 0.f;
if ( widget->getLayoutWidthPolicy() == SizePolicy::Fixed )
spec = sanitizeFloat( widget->getPropertyWidth() );
for ( Node* child = node->getFirstChild(); child; child = child->getNextNode() )
spec = std::max( spec, self( self, child ) );
return spec;
};
if ( mRows.empty() ) {
mMinIntrinsicWidth = mMaxIntrinsicWidth =
mContainer->getPixelsContentOffset().Left + mContainer->getPixelsContentOffset().Right;
mIntrinsicWidthsDirty = false;
return;
}
mRowCellOffsets.push_back( 0 );
size_t maxCols = 0;
for ( auto* row : mRows ) {
size_t colCount = 0;
for ( Node* child = row->getFirstChild(); child; child = child->getNextNode() ) {
if ( child->getType() == UI_TYPE_HTML_TABLE_CELL ) {
auto* cell = child->asType<UIHTMLTableCell>();
mCells.push_back( cell );
colCount += cell->getColSpan();
if ( mCellpadding > 0 && cell->getPadding() == Rectf::Zero ) {
cell->setPadding( { mCellpadding, mCellpadding, mCellpadding, mCellpadding } );
}
}
}
mRowCellOffsets.push_back( static_cast<Uint32>( mCells.size() ) );
maxCols = std::max( maxCols, colCount );
}
if ( maxCols == 0 ) {
mMinIntrinsicWidth = mMaxIntrinsicWidth =
mContainer->getPixelsContentOffset().Left + mContainer->getPixelsContentOffset().Right;
mIntrinsicWidthsDirty = false;
return;
}
mColMinWidths.assign( maxCols, 0.f );
mColMaxWidths.assign( maxCols, 0.f );
mColSpecifiedWidths.assign( maxCols, 0.f ); // 0 = no explicit width
if ( mTableLayout == TableLayout::Fixed ) {
if ( !mRows.empty() ) {
Uint32 start = mRowCellOffsets[0];
Uint32 end = mRowCellOffsets[1];
Uint32 colIndex = 0;
// PASS 1: Single colspan first row
for ( Uint32 i = 0; i < end - start; ++i ) {
UIHTMLTableCell* cell = mCells[start + i];
Float cellSpecified = sanitizeFloat(
std::max( cell->getPropertyWidth(),
getRecursiveSpecifiedWidth( getRecursiveSpecifiedWidth, cell ) ) );
Uint32 colspan = cell->getColSpan();
if ( colspan == 1 && colIndex < maxCols ) {
if ( cellSpecified > 0.f ) {
mColSpecifiedWidths[colIndex] =
std::max( mColSpecifiedWidths[colIndex], cellSpecified );
}
}
colIndex += colspan;
}
// PASS 2: Multi-colspan cells first row
colIndex = 0;
for ( Uint32 i = 0; i < end - start; ++i ) {
UIHTMLTableCell* cell = mCells[start + i];
Float cellSpecified = sanitizeFloat(
std::max( cell->getPropertyWidth(),
getRecursiveSpecifiedWidth( getRecursiveSpecifiedWidth, cell ) ) );
Uint32 colspan = cell->getColSpan();
if ( colspan > 1 && cellSpecified > 0.f ) {
Float curSpec = 0.f;
for ( Uint32 j = 0; j < colspan && colIndex + j < maxCols; ++j )
curSpec += mColSpecifiedWidths[colIndex + j];
Float extraSpec = std::max( 0.f, cellSpecified - curSpec );
if ( extraSpec > 0.f ) {
Float add = extraSpec / colspan;
for ( Uint32 j = 0; j < colspan && colIndex + j < maxCols; ++j )
mColSpecifiedWidths[colIndex + j] =
std::max( mColSpecifiedWidths[colIndex + j], add );
}
}
colIndex += colspan;
}
}
} else {
// PASS 1: Collect intrinsic + explicit widths (single colspan first)
for ( size_t r = 0; r < mRows.size(); ++r ) {
Uint32 start = mRowCellOffsets[r];
Uint32 end = mRowCellOffsets[r + 1];
Uint32 colIndex = 0;
for ( Uint32 i = 0; i < end - start; ++i ) {
UIHTMLTableCell* cell = mCells[start + i];
auto widthPolicy = cell->getLayoutWidthPolicy();
cell->mWidthPolicy = SizePolicy::WrapContent;
Float cellMin = sanitizeFloat( cell->getMinIntrinsicWidth() );
Float cellMax = sanitizeFloat( cell->getMaxIntrinsicWidth() );
Float cellSpecified = sanitizeFloat(
std::max( cell->getPropertyWidth(),
getRecursiveSpecifiedWidth( getRecursiveSpecifiedWidth, cell ) ) );
cell->mWidthPolicy = widthPolicy;
Uint32 colspan = cell->getColSpan();
if ( colspan == 1 && colIndex < maxCols ) {
mColMinWidths[colIndex] = std::max( mColMinWidths[colIndex], cellMin );
mColMaxWidths[colIndex] = std::max( mColMaxWidths[colIndex], cellMax );
if ( cellSpecified > 0.f ) {
mColSpecifiedWidths[colIndex] =
std::max( mColSpecifiedWidths[colIndex], cellSpecified );
}
}
colIndex += colspan;
}
}
// PASS 2: Multi-colspan cells - distribute excess only
for ( size_t r = 0; r < mRows.size(); ++r ) {
Uint32 start = mRowCellOffsets[r];
Uint32 end = mRowCellOffsets[r + 1];
Uint32 colIndex = 0;
for ( Uint32 i = 0; i < end - start; ++i ) {
UIHTMLTableCell* cell = mCells[start + i];
auto widthPolicy = cell->getLayoutWidthPolicy();
cell->mWidthPolicy = SizePolicy::WrapContent;
Float cellMin = sanitizeFloat( cell->getMinIntrinsicWidth() );
Float cellMax = sanitizeFloat( cell->getMaxIntrinsicWidth() );
Float cellSpecified = sanitizeFloat(
std::max( cell->getPropertyWidth(),
getRecursiveSpecifiedWidth( getRecursiveSpecifiedWidth, cell ) ) );
cell->mWidthPolicy = widthPolicy;
Uint32 colspan = cell->getColSpan();
if ( colspan > 1 ) {
// Min excess
Float curMin = 0.f;
for ( Uint32 j = 0; j < colspan && colIndex + j < maxCols; ++j )
curMin += mColMinWidths[colIndex + j];
Float extraMin = std::max( 0.f, cellMin - curMin );
if ( extraMin > 0.f ) {
Float add = extraMin / colspan;
for ( Uint32 j = 0; j < colspan && colIndex + j < maxCols; ++j )
mColMinWidths[colIndex + j] += add;
}
// Max excess
Float curMax = 0.f;
for ( Uint32 j = 0; j < colspan && colIndex + j < maxCols; ++j )
curMax += mColMaxWidths[colIndex + j];
Float extraMax = std::max( 0.f, cellMax - curMax );
if ( extraMax > 0.f ) {
Float add = extraMax / colspan;
for ( Uint32 j = 0; j < colspan && colIndex + j < maxCols; ++j )
mColMaxWidths[colIndex + j] += add;
}
// Specified width excess
if ( cellSpecified > 0.f ) {
Float curSpec = 0.f;
for ( Uint32 j = 0; j < colspan && colIndex + j < maxCols; ++j )
curSpec += mColSpecifiedWidths[colIndex + j];
Float extraSpec = std::max( 0.f, cellSpecified - curSpec );
if ( extraSpec > 0.f ) {
Float add = extraSpec / colspan;
for ( Uint32 j = 0; j < colspan && colIndex + j < maxCols; ++j )
mColSpecifiedWidths[colIndex + j] =
std::max( mColSpecifiedWidths[colIndex + j], add );
}
}
}
colIndex += colspan;
}
}
}
Float totalMin = 0.f, totalMax = 0.f;
for ( size_t i = 0; i < maxCols; ++i ) {
mColMinWidths[i] = sanitizeFloat( std::max( mColMinWidths[i], mColSpecifiedWidths[i] ) );
mColMaxWidths[i] = sanitizeFloat( std::max( mColMaxWidths[i], mColSpecifiedWidths[i] ) );
totalMin += mColMinWidths[i];
totalMax += mColMaxWidths[i];
}
mMinIntrinsicWidth = totalMin + mContainer->getPixelsContentOffset().Left +
mContainer->getPixelsContentOffset().Right + ( maxCols + 1 ) * mCellspacing;
mMaxIntrinsicWidth = totalMax + mContainer->getPixelsContentOffset().Left +
mContainer->getPixelsContentOffset().Right + ( maxCols + 1 ) * mCellspacing;
mIntrinsicWidthsDirty = false;
}
void TableLayouter::updateLayout() {
if ( !mContainer->isVisible() )
return;
if ( mPacking )
return;
mPacking = true;
setMatchParentIfNeededVerticalGrowth();
const StyleSheetProperty* prop = nullptr;
UIWidget* widget = mContainer->asType<UIWidget>();
if ( widget->getLayoutWidthPolicy() == SizePolicy::Fixed && widget->getUIStyle() &&
( prop = widget->getUIStyle()->getProperty( PropertyId::Width ) ) ) {
widget->asType<UINode>()->setInternalPixelsSize(
{ widget->lengthFromValue( *prop ), widget->getPixelsSize().getHeight() } );
}
computeIntrinsicWidths();
if ( mRows.empty() ) {
mPacking = false;
return;
}
size_t maxCols = mColMinWidths.size();
mColWidths.assign( maxCols, 0.f );
Float paddingH = mContainer->getPixelsContentOffset().Left + mContainer->getPixelsContentOffset().Right;
Float containerWidth = mContainer->getPixelsSize().getWidth();
Float availableWidth = sanitizeFloat(
std::max( 0.f, containerWidth - paddingH - ( maxCols + 1 ) * mCellspacing ) );
if ( availableWidth <= 0.f || maxCols == 0 ) {
mPacking = false;
return;
}
Float totalMin = 0.f;
Float totalMax = 0.f;
for ( size_t i = 0; i < maxCols; ++i ) {
totalMin += sanitizeFloat( mColMinWidths[i] );
totalMax += sanitizeFloat( mColMaxWidths[i] );
}
Float tableUsedWidth = availableWidth;
// Assign column widths
if ( mTableLayout == TableLayout::Fixed ) {
Float sumOfSpecifiedWidths = 0.f;
size_t unspecifiedCount = 0;
for ( size_t i = 0; i < maxCols; ++i ) {
if ( mColSpecifiedWidths[i] > 0.f ) {
sumOfSpecifiedWidths += mColSpecifiedWidths[i];
mColWidths[i] = mColSpecifiedWidths[i];
} else {
unspecifiedCount++;
}
}
Float remainingSpace = std::max( 0.f, availableWidth - sumOfSpecifiedWidths );
if ( unspecifiedCount > 0 ) {
Float share = remainingSpace / static_cast<Float>( unspecifiedCount );
for ( size_t i = 0; i < maxCols; ++i ) {
if ( mColSpecifiedWidths[i] <= 0.f ) {
mColWidths[i] = share;
}
}
} else if ( remainingSpace > 0.f && sumOfSpecifiedWidths > 0.f ) {
for ( size_t i = 0; i < maxCols; ++i ) {
Float scale = mColSpecifiedWidths[i] / sumOfSpecifiedWidths;
mColWidths[i] += remainingSpace * scale;
}
}
} else if ( tableUsedWidth <= totalMin + 0.001f ) {
Float scale = totalMin > 0.001f ? ( tableUsedWidth / totalMin ) : 0.f;
for ( size_t i = 0; i < maxCols; ++i )
mColWidths[i] = mColMinWidths[i] * scale;
} else if ( tableUsedWidth <= totalMax + 0.001f ) {
Float extraSpace = tableUsedWidth - totalMin;
Float totalFlex = 0.f;
for ( size_t i = 0; i < maxCols; ++i ) {
Float flex = mColMaxWidths[i] - mColMinWidths[i];
if ( mColSpecifiedWidths[i] > 0.f )
flex = 0.f;
totalFlex += flex;
}
if ( totalFlex > 0.001f ) {
for ( size_t i = 0; i < maxCols; ++i ) {
Float flex = mColMaxWidths[i] - mColMinWidths[i];
if ( mColSpecifiedWidths[i] > 0.f )
flex = 0.f;
Float added = extraSpace * ( flex / totalFlex );
mColWidths[i] = mColMinWidths[i] + added;
}
} else {
Float scale = totalMin > 0.001f ? ( tableUsedWidth / totalMin ) : 0.f;
for ( size_t i = 0; i < maxCols; ++i )
mColWidths[i] = mColMinWidths[i] * scale;
}
} else {
Float leftOver = tableUsedWidth - totalMax;
Float totalMaxUnspecified = 0.f;
size_t unspecifiedCount = 0;
for ( size_t i = 0; i < maxCols; ++i ) {
if ( mColSpecifiedWidths[i] <= 0.f ) {
totalMaxUnspecified += mColMaxWidths[i];
unspecifiedCount++;
}
}
if ( unspecifiedCount > 0 ) {
if ( totalMaxUnspecified > 0.001f ) {
for ( size_t i = 0; i < maxCols; ++i ) {
if ( mColSpecifiedWidths[i] <= 0.f ) {
Float scale = mColMaxWidths[i] / totalMaxUnspecified;
mColWidths[i] = mColMaxWidths[i] + ( leftOver * scale );
} else {
mColWidths[i] = mColMaxWidths[i];
}
}
} else {
Float share = leftOver / static_cast<Float>( unspecifiedCount );
for ( size_t i = 0; i < maxCols; ++i ) {
if ( mColSpecifiedWidths[i] <= 0.f ) {
mColWidths[i] = mColMaxWidths[i] + share;
} else {
mColWidths[i] = mColMaxWidths[i];
}
}
}
} else {
Float scale = totalMax > 0.001f ? ( tableUsedWidth / totalMax ) : 0.f;
for ( size_t i = 0; i < maxCols; ++i )
mColWidths[i] = mColMaxWidths[i] * scale;
}
}
Float sum = 0.f;
for ( float w : mColWidths )
sum += w;
if ( sum < 1.f && maxCols > 0 ) {
Float w = tableUsedWidth / static_cast<Float>( maxCols );
for ( size_t i = 0; i < maxCols; ++i )
mColWidths[i] = w;
}
for ( float& w : mColWidths )
w = sanitizeFloat( w );
Float headHeight = 0;
Float bodyHeight = 0;
Float footerHeight = 0;
size_t rowCount = mRows.size();
for ( size_t r = 0; r < rowCount; ++r ) {
Float rowHeight = 0;
Uint32 start = mRowCellOffsets[r];
Uint32 end = mRowCellOffsets[r + 1];
Uint32 columnCount = end - start;
Uint32 colIndex = 0;
for ( Uint32 c = 0; c < columnCount; ++c ) {
UIHTMLTableCell* cell = mCells[start + c];
cell->beginAttributesTransaction();
cell->setLayoutWidthPolicy( SizePolicy::Fixed );
cell->setLayoutHeightPolicy( SizePolicy::WrapContent );
Uint32 cellColspan = cell->getColSpan();
Float cellWidth = 0;
for ( Uint32 j = 0; j < cellColspan && ( colIndex + j ) < maxCols; ++j ) {
cellWidth += mColWidths[colIndex + j];
}
if ( cellColspan > 1 )
cellWidth += ( cellColspan - 1 ) * mCellspacing;
cell->setPixelsSize( cellWidth, cell->getPixelsSize().getHeight() );
cell->updateLayout();
cell->setLayoutHeightPolicy( SizePolicy::Fixed );
cell->endAttributesTransaction();
rowHeight = std::max( rowHeight, cell->getPixelsSize().getHeight() );
colIndex += cellColspan;
}
Float currentX = mCellspacing;
colIndex = 0;
for ( Uint32 c = 0; c < columnCount; ++c ) {
UIHTMLTableCell* cell = mCells[start + c];
cell->beginAttributesTransaction();
cell->setPixelsPosition( currentX, 0 );
Uint32 cellColspan = cell->getColSpan();
Float cellWidth = 0;
for ( Uint32 j = 0; j < cellColspan && ( colIndex + j ) < maxCols; ++j ) {
cellWidth += mColWidths[colIndex + j];
}
if ( cellColspan > 1 )
cellWidth += ( cellColspan - 1 ) * mCellspacing;
cell->setPixelsSize( cellWidth, rowHeight );
cell->endAttributesTransaction();
currentX += cellWidth + mCellspacing;
colIndex += cellColspan;
}
UIHTMLTableRow* row = mRows[r];
row->setPixelsSize( containerWidth - paddingH, rowHeight );
if ( r == 0 && mCells[start]->getParent()->isType( UI_TYPE_HTML_TABLE_HEAD ) ) {
headHeight = rowHeight;
} else if ( r == rowCount - 1 && columnCount &&
mCells[start]->getParent()->isType( UI_TYPE_HTML_TABLE_FOOTER ) ) {
footerHeight = rowHeight;
} else {
bodyHeight += rowHeight;
}
}
if ( mHead ) {
mHead->setPixelsPosition( 0, 0 );
mHead->setPixelsSize( { mContainer->getPixelsSize().x, headHeight } );
}
if ( mBody ) {
mBody->setPixelsPosition( 0, headHeight );
mBody->setPixelsSize( { mContainer->getPixelsSize().x, bodyHeight } );
}
if ( mFooter ) {
mFooter->setPixelsPosition( 0, headHeight + bodyHeight );
mFooter->setPixelsSize( { mContainer->getPixelsSize().x, footerHeight } );
}
Float currentY = mContainer->getPixelsContentOffset().Top + mCellspacing - headHeight;
for ( size_t r = 0; r < rowCount; ++r ) {
UIHTMLTableRow* row = mRows[r];
row->setPixelsPosition( mContainer->getPixelsContentOffset().Left, currentY );
currentY += row->getPixelsSize().getHeight() + mCellspacing;
}
if ( mHead && !mRows.empty() )
mRows[0]->setPixelsPosition( mContainer->getPixelsContentOffset().Left, 0 );
if ( mFooter && !mRows.empty() )
mRows[rowCount - 1]->setPixelsPosition( mContainer->getPixelsContentOffset().Left, 0 );
if ( mContainer->getLayoutHeightPolicy() == SizePolicy::WrapContent ) {
mContainer->asType<UINode>()->setInternalPixelsHeight(
mContainer->getPixelsContentOffset().Top + headHeight + bodyHeight + footerHeight +
( rowCount + 1 ) * mCellspacing + mContainer->getPixelsContentOffset().Bottom );
}
mPacking = false;
}
}} // namespace EE::UI

View File

@@ -105,7 +105,7 @@ UICodeEditorSplitter::UICodeEditorSplitter( UICodeEditorSplitter::Client* client
? initColorScheme
: colorSchemes[0].getName();
} else {
mColorSchemes["default"] = SyntaxColorScheme::getDefault();
mColorSchemes["default"] = SyntaxColorScheme::getDefaultDark();
mCurrentColorScheme = "default";
}
}

View File

@@ -849,7 +849,7 @@ void UIDiffView::loadFromFile( const std::string& oldFilePath, const std::string
FileSystem::fileGet( oldFilePath, oldText );
FileSystem::fileGet( newFilePath, newText );
auto def = SyntaxDefinitionManager::instance()->getByExtension( oldFilePath );
auto def = SyntaxDefinitionManager::instance()->getByExtension( newFilePath );
mSyntaxDef =
SyntaxDefinitionManager::instance()->getLanguageDefinition( def.getLanguageIndex() );
mEditor->getDocument().setSyntaxDefinition( def );

Some files were not shown because too many files have changed in this diff Show More