Merge branch 'develop' into feature/sdl3
286
.agent/plans/border_box_model_plan.md
Normal 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).
|
||||
555
.agent/plans/inline_svg_support_plan.md
Normal 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.
|
||||
61
.agent/plans/layout_separation_float_plan.md
Normal 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)
|
||||
55
.agent/plans/layout_separation_form_plan.md
Normal 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)
|
||||
33
.agent/rules/html-layout-architecture.md
Normal 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.
|
||||
@@ -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):*
|
||||
|
||||
30
.github/workflows/ecode-nightly.yml
vendored
@@ -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++
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
54
bin/unit_tests/assets/html/absolute_position.html
Normal 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>
|
||||
65
bin/unit_tests/assets/html/anchor_margins.html
Normal 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>
|
||||
128
bin/unit_tests/assets/html/border_tests.html
Normal 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>
|
||||
86
bin/unit_tests/assets/html/border_tests_2.html
Normal 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>
|
||||
BIN
bin/unit_tests/assets/html/eepp-ui-anchor-margins.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
bin/unit_tests/assets/html/eepp-ui-border-rendering-2.webp
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
bin/unit_tests/assets/html/eepp-ui-border-rendering.webp
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
bin/unit_tests/assets/html/eepp-ui-span-padding.webp
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
128
bin/unit_tests/assets/html/inline_block_wrap.html
Normal 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'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'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>
|
||||
176
bin/unit_tests/assets/html/position_absolute_and_float.html
Normal 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>
|
||||
3956
bin/unit_tests/assets/html/reddit_header.html
Normal file
56
bin/unit_tests/assets/html/span_padding.html
Normal 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>
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
46
include/eepp/network/cookiemanager.hpp
Normal 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
|
||||
@@ -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();
|
||||
|
||||
@@ -122,6 +122,10 @@ class EE_API Event {
|
||||
OnFoldUnfoldRange,
|
||||
OnResourceLoaded,
|
||||
OnDiscard,
|
||||
OnNavigationStarted,
|
||||
OnNavigationCompleted,
|
||||
OnNavigationError,
|
||||
OnTitleChanged,
|
||||
NoEvent = eeINDEX_NOT_FOUND
|
||||
};
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
28
include/eepp/ui/blocklayouter.hpp
Normal 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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
82
include/eepp/ui/csslayouttypes.hpp
Normal 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
|
||||
@@ -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 );
|
||||
|
||||
|
||||
@@ -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 );
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
17
include/eepp/ui/inlinelayouter.hpp
Normal 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
|
||||
17
include/eepp/ui/nonelayouter.hpp
Normal 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
|
||||
59
include/eepp/ui/tablelayouter.hpp
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
56
include/eepp/ui/uihtmlform.hpp
Normal 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
|
||||
34
include/eepp/ui/uihtmlimage.hpp
Normal 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
|
||||
@@ -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();
|
||||
|
||||
51
include/eepp/ui/uihtmllistitem.hpp
Normal 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
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
91
include/eepp/ui/uihtmlwidget.hpp
Normal 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
|
||||
@@ -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
|
||||
|
||||
37
include/eepp/ui/uilayouter.hpp
Normal 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
|
||||
19
include/eepp/ui/uilayoutermanager.hpp
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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 );
|
||||
};
|
||||
|
||||
35
include/eepp/ui/uitextnode.hpp
Normal 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
|
||||
@@ -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 );
|
||||
|
||||
|
||||
114
include/eepp/ui/uiwebview.hpp
Normal 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
|
||||
@@ -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 );
|
||||
};
|
||||
|
||||
|
||||
@@ -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 ) \
|
||||
|
||||
@@ -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" \
|
||||
|
||||
190
src/eepp/graphics/csslayouttypes.cpp
Normal 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
|
||||
@@ -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 ) ) );
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ) {
|
||||
|
||||
79
src/eepp/network/cookiemanager.cpp
Normal 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
|
||||
@@ -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() )
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
273
src/eepp/ui/blocklayouter.cpp
Normal 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
|
||||
@@ -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 );
|
||||
|
||||
@@ -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&,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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, "" );
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 );
|
||||
|
||||
535
src/eepp/ui/tablelayouter.cpp
Normal 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
|
||||
@@ -105,7 +105,7 @@ UICodeEditorSplitter::UICodeEditorSplitter( UICodeEditorSplitter::Client* client
|
||||
? initColorScheme
|
||||
: colorSchemes[0].getName();
|
||||
} else {
|
||||
mColorSchemes["default"] = SyntaxColorScheme::getDefault();
|
||||
mColorSchemes["default"] = SyntaxColorScheme::getDefaultDark();
|
||||
mCurrentColorScheme = "default";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 );
|
||||
|
||||