From 1970b8bbe992acdbf723b8f38a577e83c76b0114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sat, 25 Apr 2026 17:27:45 -0300 Subject: [PATCH 1/2] WIP CSS Display support. --- .agent/plans/layout_separation_plan.md | 98 ++++ include/eepp/ui/blocklayouter.hpp | 28 + include/eepp/ui/css/propertydefinition.hpp | 7 + include/eepp/ui/csslayouttypes.hpp | 39 ++ include/eepp/ui/inlinelayouter.hpp | 17 + include/eepp/ui/nonelayouter.hpp | 17 + include/eepp/ui/tablelayouter.hpp | 51 ++ include/eepp/ui/uihelper.hpp | 1 + include/eepp/ui/uihtmltable.hpp | 42 +- include/eepp/ui/uihtmlwidget.hpp | 63 +++ include/eepp/ui/uilayout.hpp | 6 +- include/eepp/ui/uilayouter.hpp | 37 ++ include/eepp/ui/uilayoutermanager.hpp | 19 + include/eepp/ui/uinode.hpp | 3 + include/eepp/ui/uirichtext.hpp | 25 +- include/eepp/ui/uitextspan.hpp | 3 +- include/eepp/ui/uiwidget.hpp | 2 +- src/eepp/graphics/csslayouttypes.cpp | 88 +++ src/eepp/ui/blocklayouter.cpp | 243 ++++++++ src/eepp/ui/css/stylesheetspecification.cpp | 8 + src/eepp/ui/tablelayouter.cpp | 533 ++++++++++++++++++ src/eepp/ui/uihtmltable.cpp | 589 ++------------------ src/eepp/ui/uihtmlwidget.cpp | 158 ++++++ src/eepp/ui/uilayouter.cpp | 10 + src/eepp/ui/uilayoutermanager.cpp | 28 + src/eepp/ui/uirichtext.cpp | 290 +++------- src/eepp/ui/uitextspan.cpp | 3 +- 27 files changed, 1613 insertions(+), 795 deletions(-) create mode 100644 .agent/plans/layout_separation_plan.md create mode 100644 include/eepp/ui/blocklayouter.hpp create mode 100644 include/eepp/ui/csslayouttypes.hpp create mode 100644 include/eepp/ui/inlinelayouter.hpp create mode 100644 include/eepp/ui/nonelayouter.hpp create mode 100644 include/eepp/ui/tablelayouter.hpp create mode 100644 include/eepp/ui/uihtmlwidget.hpp create mode 100644 include/eepp/ui/uilayouter.hpp create mode 100644 include/eepp/ui/uilayoutermanager.hpp create mode 100644 src/eepp/graphics/csslayouttypes.cpp create mode 100644 src/eepp/ui/blocklayouter.cpp create mode 100644 src/eepp/ui/tablelayouter.cpp create mode 100644 src/eepp/ui/uihtmlwidget.cpp create mode 100644 src/eepp/ui/uilayouter.cpp create mode 100644 src/eepp/ui/uilayoutermanager.cpp diff --git a/.agent/plans/layout_separation_plan.md b/.agent/plans/layout_separation_plan.md new file mode 100644 index 000000000..4fcfa6373 --- /dev/null +++ b/.agent/plans/layout_separation_plan.md @@ -0,0 +1,98 @@ +# UI Layout Separation & CSS Display/Position Support Plan (Strict Implementation Guide) + +This document outlines the architectural plan for decoupling layout logic from specific widgets (like `UIRichText` and `UIHTMLTable`), introducing a robust generic layouter system, and supporting standard CSS `display` and `position` properties. + +**AGENT DIRECTIVE (CRITICAL):** You are Negen. Fulfill this plan iteratively. 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 X.Y passed"`) upon passing a step. + +--- + +## IMPLEMENTATION HAZARDS (READ BEFORE CODING) +When migrating logic from Widgets to Layouters, you will face these traps: +1. **Pixel vs DP APIs:** Widgets have `getSize()` and `getPixelsSize()`, `getPadding()` and `getPixelsPadding()`. **Layouters MUST exclusively use the `Pixels` variants** (`getPixelsSize()`, `getPixelsPadding()`, `getLayoutPixelsMargin()`). Using the non-pixel variants will cause massive visual regressions due to DPI scaling. +2. **Infinite Recursion:** When a Layouter delegates to a widget's intrinsic width calculation, or vice versa, you must strictly manage the dirty flags (e.g., `mIntrinsicWidthsDirty = false`). Failure to clear these flags inside `computeIntrinsicWidths` will cause stack overflows. +3. **Table Hierarchy:** In HTML/eepp tables, the hierarchy is `Table` -> `TableSection` (`thead`, `tbody`) -> `TableRow` (`tr`) -> `TableCell` (`td`). The original code often checks `row->getParent()->isType(...)`. If you change sections to `UIHTMLWidget`, be extremely careful not to accidentally assign them a `BlockLayouter` that overrides the `TableLayouter`'s positioning. + +--- + +## Phase 1: Core Infrastructure + +**Step 1.1: CSS Properties & Enums** +- Add `Display`, `Position`, `Top`, `Right`, `Bottom`, `Left`, `ZIndex` to `PropertyId` enum (`propertydefinition.hpp`). +- Create `CSSDisplay` and `CSSPosition` enums (`csslayouttypes.hpp`). +- **Validation:** Compile and run all tests. Must pass. (Snapshot) + +**Step 1.2: Layouter Interfaces & Manager** +- Create `UILayouter` base interface (`uilayouter.hpp`). Must hold `UIWidget* mContainer` and `bool mValid`. +- Create `UILayouterManager` (`uilayoutermanager.hpp`/`cpp`) to spawn layouters. +- Create empty skeletons for `BlockLayouter`, `InlineLayouter`, `TableLayouter`, `NoneLayouter`. +- **CRITICAL:** Run `premake4` to regenerate makefiles now that new files are added. +- **Validation:** Compile and run all tests. Must pass. (Snapshot) + +--- + +## Phase 2: UIHTMLWidget Base Class + +**Step 2.1: Implement UIHTMLWidget** +- Create `UIHTMLWidget` inheriting from `UILayout`. +- Add CSS properties (`mDisplay`, `mPosition`, offsets, `mZIndex`). +- Implement `getLayouter()` which lazily instantiates via `UILayouterManager`. +- Override `onChildCountChange` and `onDisplayChange` to call `if (mLayouter) mLayouter->invalidate()`. +- **Validation:** Compile and run all tests. Must pass. (Snapshot) + +--- + +## Phase 3: UIRichText & BlockLayouter (High Risk) + +**Step 3.1: Inheritance and Access Control** +- Change `UIRichText` and `UITextSpan` to inherit from `UIHTMLWidget`. +- Add `BlockLayouter` and `InlineLayouter` as `friend class` to `UIRichText`, `UILayout`, and `UIWidget`. +- **CRITICAL:** In `UIHTMLWidget::getLayouter()`, temporarily return `nullptr`. We want `UIRichText` to still use its monolithic logic while we set up inheritance. +- **Validation:** Compile and run `UIRichText.*` tests. Must pass exactly. (Snapshot) + +**Step 3.2: Implement BlockLayouter** +- Copy `UIRichText::updateLayout()`, `getMinIntrinsicWidth()`, and `getMaxIntrinsicWidth()` into `BlockLayouter`. +- **INVARIANTS TO MAINTAIN:** + - You MUST use `getPixelsPadding()` everywhere `mPaddingPx` was used. + - You MUST use `getPixelsSize()` everywhere `mSize` was used. + - The `mResizedCount` loop must be preserved: if `richText->mResizedCount > 0` after `setInternalPixelsWidth/Height`, `positionRichTextChildren` must run again. + - Do NOT skip `MatchParent` children in the `positionRichTextChildren` while loop. +- Enable `BlockLayouter` for `UIRichText` (but explicitly disable it for `UI_TYPE_HTML_TABLE_CELL` for now to isolate bugs). +- **Validation:** Compile and run `UIRichText.*` tests. **Zero pixel difference allowed.** (Snapshot) + +**Step 3.3: Implement InlineLayouter** +- Replicate inline logic for `UITextSpan` into `InlineLayouter`. +- **Validation:** Compile and run all tests. (Snapshot) + +--- + +## Phase 4: UIHTMLTable Refactoring (High Risk) + +**Step 4.1: Table Base Classes & Friends** +- Change `UIHTMLTable`, `UIHTMLTableRow`, `UIHTMLTableHead`, `UIHTMLTableBody`, `UIHTMLTableFooter` to inherit from `UIHTMLWidget`. +- Make `TableLayouter` a friend of `UIHTMLTable`. +- **CRITICAL:** `UILayouterManager` MUST return `nullptr` for Table Sections and Table Rows. Only the `Table` itself gets `TableLayouter`. +- **Validation:** Compile and run `UIHTMLTable.*` tests. (Snapshot) + +**Step 4.2: Implement TableLayouter** +- Move `UIHTMLTable::updateLayout()` and `computeIntrinsicWidths()` into `TableLayouter`. +- **INVARIANTS TO MAINTAIN:** + - `computeIntrinsicWidths` MUST clear `table->mIntrinsicWidthsDirty = false;` at all exit points to prevent recursion. + - `currentY` for rows MUST be accumulated exactly as: `Float currentY = padding.Top + mCellspacing - headHeight;` then incremented by `rowHeight + mCellspacing`. Do not attempt to "fix" this math; it offsets based on section anchors. + - When determining `headHeight`, use `row->getParent()->isType(UI_TYPE_HTML_TABLE_HEAD)`. Do not check the cell's parent. +- Enable `TableLayouter` in `getLayouter()`. +- **Validation:** Compile and run `UIHTMLTable.*` tests. **Zero pixel difference allowed.** (Snapshot) + +**Step 4.3: Unify TableCell with BlockLayouter** +- Now that TableLayouter is proven, allow `UILayouterManager` to return `BlockLayouter` for `CSSDisplay::TableCell`. +- Ensure `BlockLayouter` does NOT override fixed widths if the container is a `TableCell` (the table layouter owns the cell width). +- **Validation:** Run all tests. (Snapshot) + +--- + +## Phase 5: CSS Position (Out-of-Flow) + +**Step 5.1: Position Implementation** +- Implement `getContainingBlock()` in `UIHTMLWidget`. +- Update layouters to skip children with `position: absolute|fixed`. +- Add `positionOutOfFlowChildren()` to `UIHTMLWidget::updateLayout()` after the layouter finishes. +- **Validation:** Compile and run all tests. Existing UI should remain unaffected. (Snapshot) \ No newline at end of file diff --git a/include/eepp/ui/blocklayouter.hpp b/include/eepp/ui/blocklayouter.hpp new file mode 100644 index 000000000..7b1366d86 --- /dev/null +++ b/include/eepp/ui/blocklayouter.hpp @@ -0,0 +1,28 @@ +#ifndef EE_UI_BLOCKLAYOUTER_HPP +#define EE_UI_BLOCKLAYOUTER_HPP + +#include + +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 diff --git a/include/eepp/ui/css/propertydefinition.hpp b/include/eepp/ui/css/propertydefinition.hpp index 6cbff682e..2b212b97e 100644 --- a/include/eepp/ui/css/propertydefinition.hpp +++ b/include/eepp/ui/css/propertydefinition.hpp @@ -239,6 +239,13 @@ enum class PropertyId : Uint32 { Rows = String::hash( "rows" ), Cols = String::hash( "cols" ), InputMode = String::hash( "input-mode" ), + 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" ), }; enum class PropertyType : Uint32 { diff --git a/include/eepp/ui/csslayouttypes.hpp b/include/eepp/ui/csslayouttypes.hpp new file mode 100644 index 000000000..743865f60 --- /dev/null +++ b/include/eepp/ui/csslayouttypes.hpp @@ -0,0 +1,39 @@ +#ifndef EE_UI_CSSLAYOUTTYPES_HPP +#define EE_UI_CSSLAYOUTTYPES_HPP + +#include +#include + +namespace EE { namespace UI { + +enum class CSSDisplay { + Inline, + Block, + InlineBlock, + Flex, + None, + Table, + TableRow, + TableCell, + TableHead, + TableBody, + TableFooter +}; + +struct CSSDisplayHelper { + static std::string toString( CSSDisplay display ); + + static CSSDisplay fromString( std::string_view val ); +}; + +enum class CSSPosition { Static, Relative, Absolute, Fixed, Sticky }; + +struct CSSPositionHelper { + static std::string toString( CSSPosition position ); + + static CSSPosition fromString( std::string_view val ); +}; + +}} // namespace EE::UI + +#endif diff --git a/include/eepp/ui/inlinelayouter.hpp b/include/eepp/ui/inlinelayouter.hpp new file mode 100644 index 000000000..e088697c7 --- /dev/null +++ b/include/eepp/ui/inlinelayouter.hpp @@ -0,0 +1,17 @@ +#ifndef EE_UI_INLINELAYOUTER_HPP +#define EE_UI_INLINELAYOUTER_HPP + +#include + +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 diff --git a/include/eepp/ui/nonelayouter.hpp b/include/eepp/ui/nonelayouter.hpp new file mode 100644 index 000000000..15c747ecd --- /dev/null +++ b/include/eepp/ui/nonelayouter.hpp @@ -0,0 +1,17 @@ +#ifndef EE_UI_NONELAYOUTER_HPP +#define EE_UI_NONELAYOUTER_HPP + +#include + +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 diff --git a/include/eepp/ui/tablelayouter.hpp b/include/eepp/ui/tablelayouter.hpp new file mode 100644 index 000000000..c612c1a47 --- /dev/null +++ b/include/eepp/ui/tablelayouter.hpp @@ -0,0 +1,51 @@ +#ifndef EE_UI_TABLELAYOUTER_HPP +#define EE_UI_TABLELAYOUTER_HPP + +#include +#include + +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 mRows; + SmallVector mColWidths; + SmallVector mCells; + SmallVector mRowCellOffsets; + SmallVector mColMinWidths; + SmallVector mColMaxWidths; + SmallVector mColSpecifiedWidths; + TableLayout mTableLayout{ TableLayout::Auto }; + UIHTMLTableHead* mHead{ nullptr }; + UIHTMLTableBody* mBody{ nullptr }; + UIHTMLTableFooter* mFooter{ nullptr }; + Float mCellpadding{ 0 }; + Float mCellspacing{ 0 }; +}; + +}} // namespace EE::UI + +#endif diff --git a/include/eepp/ui/uihelper.hpp b/include/eepp/ui/uihelper.hpp index 1f1e8db2e..23f3bfdf0 100644 --- a/include/eepp/ui/uihelper.hpp +++ b/include/eepp/ui/uihelper.hpp @@ -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, diff --git a/include/eepp/ui/uihtmltable.hpp b/include/eepp/ui/uihtmltable.hpp index 1e4394120..3cfe4b669 100644 --- a/include/eepp/ui/uihtmltable.hpp +++ b/include/eepp/ui/uihtmltable.hpp @@ -2,29 +2,18 @@ #define EE_UI_UIHTMLTABLE_HPP #include -#include +#include #include 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; @@ -41,25 +30,12 @@ class EE_API UIHTMLTable : public UILayout { virtual Uint32 onMessage( const NodeMessage* Msg ); void computeIntrinsicWidths() const; - - SmallVector mRows; - SmallVector mColWidths; - SmallVector mCells; - SmallVector mRowCellOffsets; - mutable SmallVector mColMinWidths; - mutable SmallVector mColMaxWidths; - mutable SmallVector 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 ); @@ -79,7 +55,7 @@ class EE_API UIHTMLTableCell : public UIRichText { Uint32 mColspan{ 1 }; }; -class EE_API UIHTMLTableRow : public UIWidget { +class EE_API UIHTMLTableRow : public UIHTMLWidget { public: static UIHTMLTableRow* New(); @@ -90,7 +66,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 +77,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 +88,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(); diff --git a/include/eepp/ui/uihtmlwidget.hpp b/include/eepp/ui/uihtmlwidget.hpp new file mode 100644 index 000000000..1e9e2e04a --- /dev/null +++ b/include/eepp/ui/uihtmlwidget.hpp @@ -0,0 +1,63 @@ +#ifndef EE_UI_UIHTMLWIDGET_HPP +#define EE_UI_UIHTMLWIDGET_HPP + +#include +#include + +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 ); + + const Rectf& getOffsets() const { return mOffsets; } + void setOffsets( const Rectf& offsets ); + + int getZIndex() const { return mZIndex; } + void setZIndex( int zIndex ); + + virtual std::string getPropertyString( const PropertyDefinition* propertyDef, + const Uint32& state = 0 ) const; + virtual bool applyProperty( const StyleSheetProperty& attribute ); + + virtual RichText* getRichTextPtr() { return nullptr; } + + virtual void invalidateIntrinsicSize(); + + protected: + CSSDisplay mDisplay{ CSSDisplay::Block }; + CSSPosition mPosition{ CSSPosition::Static }; + Rectf mOffsets{ 0, 0, 0, 0 }; + int mZIndex{ 0 }; + UILayouter* mLayouter{ nullptr }; +}; + +}} // namespace EE::UI + +#endif diff --git a/include/eepp/ui/uilayout.hpp b/include/eepp/ui/uilayout.hpp index 698b6580a..ad1c7714c 100644 --- a/include/eepp/ui/uilayout.hpp +++ b/include/eepp/ui/uilayout.hpp @@ -19,12 +19,14 @@ 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 ); protected: friend class UISceneNode; + friend class UILayouter; UnorderedSet mLayouts; bool mDirtyLayout{ false }; @@ -52,8 +54,6 @@ class EE_API UILayout : public UIWidget { void setLayoutDirty(); bool setMatchParentIfNeededVerticalGrowth(); - - void onAutoSizeChild( UIWidget* child ); }; }} // namespace EE::UI diff --git a/include/eepp/ui/uilayouter.hpp b/include/eepp/ui/uilayouter.hpp new file mode 100644 index 000000000..2151a550a --- /dev/null +++ b/include/eepp/ui/uilayouter.hpp @@ -0,0 +1,37 @@ +#ifndef EE_UI_UILAYOUTER_HPP +#define EE_UI_UILAYOUTER_HPP + +#include +#include + +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 diff --git a/include/eepp/ui/uilayoutermanager.hpp b/include/eepp/ui/uilayoutermanager.hpp new file mode 100644 index 000000000..dfe448518 --- /dev/null +++ b/include/eepp/ui/uilayoutermanager.hpp @@ -0,0 +1,19 @@ +#ifndef EE_UI_UILAYOUTERMANAGER_HPP +#define EE_UI_UILAYOUTERMANAGER_HPP + +#include +#include + +namespace EE { namespace UI { + +class UILayouter; +class UIWidget; + +class EE_API UILayouterManager { + public: + static UILayouter* create( CSSDisplay display, UIWidget* container ); +}; + +}} // namespace EE::UI + +#endif diff --git a/include/eepp/ui/uinode.hpp b/include/eepp/ui/uinode.hpp index dca496292..311187a6c 100644 --- a/include/eepp/ui/uinode.hpp +++ b/include/eepp/ui/uinode.hpp @@ -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. * diff --git a/include/eepp/ui/uirichtext.hpp b/include/eepp/ui/uirichtext.hpp index 09a792071..ad265075e 100644 --- a/include/eepp/ui/uirichtext.hpp +++ b/include/eepp/ui/uirichtext.hpp @@ -2,12 +2,18 @@ #define EE_UI_UIRICHTEXT_HPP #include +#include #include 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 ); @@ -127,12 +133,13 @@ class EE_API UIRichText : public UILayout { 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" ); @@ -155,9 +162,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(); }; @@ -184,6 +189,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 diff --git a/include/eepp/ui/uitextspan.hpp b/include/eepp/ui/uitextspan.hpp index 2827ae3aa..9583a939e 100644 --- a/include/eepp/ui/uitextspan.hpp +++ b/include/eepp/ui/uitextspan.hpp @@ -2,13 +2,14 @@ #define EE_UI_UITEXTSPAN_HPP #include +#include #include namespace EE { namespace UI { using SpanHitBoxes = SmallVector; -class EE_API UITextSpan : public UIWidget { +class EE_API UITextSpan : public UIHTMLWidget { public: static UITextSpan* New(); diff --git a/include/eepp/ui/uiwidget.hpp b/include/eepp/ui/uiwidget.hpp index 1d68c78b7..f5bb94529 100644 --- a/include/eepp/ui/uiwidget.hpp +++ b/include/eepp/ui/uiwidget.hpp @@ -531,7 +531,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. diff --git a/src/eepp/graphics/csslayouttypes.cpp b/src/eepp/graphics/csslayouttypes.cpp new file mode 100644 index 000000000..a50e58a7b --- /dev/null +++ b/src/eepp/graphics/csslayouttypes.cpp @@ -0,0 +1,88 @@ +#include + +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::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 == "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; +} + +}} // namespace EE::UI diff --git a/src/eepp/ui/blocklayouter.cpp b/src/eepp/ui/blocklayouter.cpp new file mode 100644 index 000000000..75945eeea --- /dev/null +++ b/src/eepp/ui/blocklayouter.cpp @@ -0,0 +1,243 @@ +#include +#include +#include +#include +#include +#include + +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(); + 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->getPixelsPadding().Left + + mContainer->getPixelsPadding().Right; + UIRichText::rebuildRichText( widget, tmpRt, UIRichText::IntrinsicMode::Max ); + mMaxIntrinsicWidth = tmpRt.getMaxIntrinsicWidth() + mContainer->getPixelsPadding().Left + + mContainer->getPixelsPadding().Right; + mIntrinsicWidthsDirty = false; + } +} + +void BlockLayouter::updateLayout() { + if ( !mContainer->isType( UI_TYPE_HTML_WIDGET ) ) + return; + + auto* widget = mContainer->asType(); + auto* rt = widget->getRichTextPtr(); + if ( rt == nullptr || mPacking ) + return; + mResizedCount = 0; + mPacking = true; + + 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->getPixelsPadding().Left + + mContainer->getPixelsPadding().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->getPixelsPadding().Top + + mContainer->getPixelsPadding().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 ); + + 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( span.block ) ) + return &span; + } + currentSpan = 0; + currentLine++; + } + return nullptr; + }; + + Int64 curCharIdx = 0; + + auto processWidget = [&]( UIWidget* widget, auto& processWidgetRef ) -> Rectf { + constexpr Float maxF = std::numeric_limits::max(); + constexpr Float lowF = std::numeric_limits::lowest(); + Rectf bounds( maxF, maxF, lowF, lowF ); + + Vector2f offset; + Node* p = widget->getParent(); + while ( p && p != mContainer ) { + offset += p->isWidget() ? p->asType()->getPixelsPosition() : p->getPosition(); + p = p->getParent(); + } + + if ( widget->isType( UI_TYPE_TEXTSPAN ) ) { + UITextSpan* textSpan = widget->asType(); + 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->getPixelsPadding().Left + rspan.position.x, + mContainer->getPixelsPadding().Top + line.y + + rspan.position.y, + mContainer->getPixelsPadding().Left + rspan.position.x + + rspan.size.getWidth(), + mContainer->getPixelsPadding().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; + } + } + + Node* spanChild = widget->getFirstChild(); + while ( spanChild != NULL ) { + if ( spanChild->isWidget() ) { + bounds.expand( + processWidgetRef( spanChild->asType(), processWidgetRef ) ); + } + 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()->getPixelsPosition(); + pos.y += widget->getPrevNode()->getPixelsSize().getHeight(); + } + widget->setPixelsPosition( pos ); + widget->setPixelsSize( { eemax( 0.f, mContainer->getPixelsSize().getWidth() - + mContainer->getPixelsPadding().Left - + mContainer->getPixelsPadding().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->getPixelsPadding().Left + span->position.x + margin.Left, + mContainer->getPixelsPadding().Top + lineY + span->position.y + margin.Top ); + + widget->setPixelsPosition( targetPos - offset ); + + bounds = Rectf( targetPos, span->size ); + } + } + return bounds; + }; + + child = mContainer->getFirstChild(); + while ( NULL != child ) { + if ( child->isWidget() ) { + processWidget( child->asType(), processWidget ); + } + child = child->getNextNode(); + } +} + +}} // namespace EE::UI diff --git a/src/eepp/ui/css/stylesheetspecification.cpp b/src/eepp/ui/css/stylesheetspecification.cpp index f778b1767..77dbc94dd 100644 --- a/src/eepp/ui/css/stylesheetspecification.cpp +++ b/src/eepp/ui/css/stylesheetspecification.cpp @@ -425,6 +425,14 @@ void StyleSheetSpecification::registerDefaultProperties() { registerProperty( "cols", "20" ).setType( PropertyType::NumberInt ); registerProperty( "input-mode", "normal" ).setType( PropertyType::String ); + registerProperty( "display", "inline" ); + registerProperty( "position", "static" ); + registerProperty( "top", "auto" ); + registerProperty( "right", "auto" ); + registerProperty( "bottom", "auto" ); + registerProperty( "left", "auto" ); + registerProperty( "z-index", "auto" ); + registerProperty( "inner-widget-orientation", "widgeticontextbox" ) .setType( PropertyType::String ); diff --git a/src/eepp/ui/tablelayouter.cpp b/src/eepp/ui/tablelayouter.cpp new file mode 100644 index 000000000..f872698de --- /dev/null +++ b/src/eepp/ui/tablelayouter.cpp @@ -0,0 +1,533 @@ +#include +#include +#include + +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() ); + } else if ( child->getType() != UI_TYPE_HTML_TABLE ) { + if ( child->getType() == UI_TYPE_HTML_TABLE_HEAD ) + mHead = child->asType(); + else if ( child->getType() == UI_TYPE_HTML_TABLE_BODY ) + mBody = child->asType(); + else if ( child->getType() == UI_TYPE_HTML_TABLE_FOOTER ) + mFooter = child->asType(); + + self( self, child ); + } + } + }; + collectRows( collectRows, mContainer ); + + auto getRecursiveSpecifiedWidth = [&]( auto&& self, Node* node ) -> Float { + if ( !node->isWidget() ) + return 0.f; + UIWidget* widget = node->asType(); + 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->getPixelsPadding().Left + mContainer->getPixelsPadding().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(); + 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( mCells.size() ) ); + maxCols = std::max( maxCols, colCount ); + } + + if ( maxCols == 0 ) { + mMinIntrinsicWidth = mMaxIntrinsicWidth = + mContainer->getPixelsPadding().Left + mContainer->getPixelsPadding().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->getPixelsPadding().Left + + mContainer->getPixelsPadding().Right + ( maxCols + 1 ) * mCellspacing; + mMaxIntrinsicWidth = totalMax + mContainer->getPixelsPadding().Left + + mContainer->getPixelsPadding().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(); + if ( widget->getLayoutWidthPolicy() == SizePolicy::Fixed && widget->getUIStyle() && + ( prop = widget->getUIStyle()->getProperty( PropertyId::Width ) ) ) { + widget->asType()->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->getPixelsPadding().Left + mContainer->getPixelsPadding().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( 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( 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( 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->getPixelsPadding().Top + mCellspacing - headHeight; + for ( size_t r = 0; r < rowCount; ++r ) { + UIHTMLTableRow* row = mRows[r]; + row->setPixelsPosition( mContainer->getPixelsPadding().Left, currentY ); + currentY += row->getPixelsSize().getHeight() + mCellspacing; + } + + if ( mHead && !mRows.empty() ) + mRows[0]->setPixelsPosition( mContainer->getPixelsPadding().Left, 0 ); + + if ( mFooter && !mRows.empty() ) + mRows[rowCount - 1]->setPixelsPosition( mContainer->getPixelsPadding().Left, 0 ); + + if ( mContainer->getLayoutHeightPolicy() == SizePolicy::WrapContent ) { + mContainer->asType()->setInternalPixelsHeight( + mContainer->getPixelsPadding().Top + headHeight + bodyHeight + footerHeight + + ( rowCount + 1 ) * mCellspacing + mContainer->getPixelsPadding().Bottom ); + } + + mPacking = false; +} + +}} // namespace EE::UI diff --git a/src/eepp/ui/uihtmltable.cpp b/src/eepp/ui/uihtmltable.cpp index 5d8013157..123b18a6e 100644 --- a/src/eepp/ui/uihtmltable.cpp +++ b/src/eepp/ui/uihtmltable.cpp @@ -1,42 +1,27 @@ -#include "eepp/ui/uistyle.hpp" -#include -#include +#include #include +#include +#include namespace EE { namespace UI { -static inline Float sanitizeFloat( Float val ) { - return std::isfinite( val ) ? val : 0.f; -} - UIHTMLTable* UIHTMLTable::New() { return eeNew( UIHTMLTable, () ); } -UIHTMLTable::UIHTMLTable() : UILayout( "table" ) { +UIHTMLTable::UIHTMLTable() : UIHTMLWidget( "table" ) { + mDisplay = CSSDisplay::Table; mFlags |= UI_HTML_ELEMENT | UI_OWNS_CHILDREN_POSITION; mWidthPolicy = SizePolicy::MatchParent; mHeightPolicy = SizePolicy::WrapContent; } -void UIHTMLTable::setTableLayout( TableLayout layout ) { - if ( layout != mTableLayout ) { - mTableLayout = layout; - invalidateIntrinsicSize(); - tryUpdateLayout(); - } -} - -TableLayout UIHTMLTable::getTableLayout() const { - return mTableLayout; -} - Uint32 UIHTMLTable::getType() const { return UI_TYPE_HTML_TABLE; } bool UIHTMLTable::isType( const Uint32& type ) const { - return UIHTMLTable::getType() == type || UILayout::isType( type ); + return UIHTMLTable::getType() == type || UIHTMLWidget::isType( type ); } bool UIHTMLTable::applyProperty( const StyleSheetProperty& attribute ) { @@ -45,22 +30,29 @@ bool UIHTMLTable::applyProperty( const StyleSheetProperty& attribute ) { switch ( attribute.getPropertyDefinition()->getPropertyId() ) { case PropertyId::Cellspacing: - mCellspacing = lengthFromValue( attribute ); - invalidateIntrinsicSize(); - tryUpdateLayout(); + if ( const_cast( this )->getLayouter() ) { + static_cast( const_cast( this )->getLayouter() ) + ->setCellspacing( lengthFromValue( attribute ) ); + invalidateIntrinsicSize(); + tryUpdateLayout(); + } return true; case PropertyId::Cellpadding: - mCellpadding = lengthFromValue( attribute ); - invalidateIntrinsicSize(); - tryUpdateLayout(); + if ( const_cast( this )->getLayouter() ) { + static_cast( const_cast( this )->getLayouter() ) + ->setCellpadding( lengthFromValue( attribute ) ); + invalidateIntrinsicSize(); + tryUpdateLayout(); + } return true; case PropertyId::TableLayout: { std::string val = attribute.asString(); String::toLowerInPlace( val ); - if ( val == "fixed" ) { - setTableLayout( TableLayout::Fixed ); - } else if ( val == "auto" ) { - setTableLayout( TableLayout::Auto ); + if ( const_cast( this )->getLayouter() ) { + static_cast( const_cast( this )->getLayouter() ) + ->setTableLayout( val == "fixed" ? TableLayout::Fixed : TableLayout::Auto ); + invalidateIntrinsicSize(); + tryUpdateLayout(); } return true; } @@ -68,521 +60,47 @@ bool UIHTMLTable::applyProperty( const StyleSheetProperty& attribute ) { break; } - return UILayout::applyProperty( attribute ); + return UIHTMLWidget::applyProperty( attribute ); } void UIHTMLTable::computeIntrinsicWidths() const { - if ( !mIntrinsicWidthsDirty ) - return; - - UIHTMLTable* me = const_cast( this ); - - me->mRows.clear(); - me->mHead = nullptr; - me->mBody = nullptr; - me->mFooter = nullptr; - me->mCells.clear(); - me->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 ) { - me->mRows.push_back( child->asType() ); - } else if ( child->getType() != UI_TYPE_HTML_TABLE ) { - if ( child->getType() == UI_TYPE_HTML_TABLE_HEAD ) - me->mHead = child->asType(); - else if ( child->getType() == UI_TYPE_HTML_TABLE_BODY ) - me->mBody = child->asType(); - else if ( child->getType() == UI_TYPE_HTML_TABLE_FOOTER ) - me->mFooter = child->asType(); - - self( self, child ); - } - } - }; - collectRows( collectRows, me ); - - auto getRecursiveSpecifiedWidth = [&]( auto&& self, Node* node ) -> Float { - if ( !node->isWidget() ) - return 0.f; - UIWidget* widget = node->asType(); - 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 = mPaddingPx.Left + mPaddingPx.Right; - mIntrinsicWidthsDirty = false; - return; - } - - me->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(); - me->mCells.push_back( cell ); - colCount += cell->getColspan(); - if ( mCellpadding > 0 && cell->getPadding() == Rectf::Zero ) { - cell->setPadding( { mCellpadding, mCellpadding, mCellpadding, mCellpadding } ); - } - } - } - me->mRowCellOffsets.push_back( static_cast( mCells.size() ) ); - maxCols = std::max( maxCols, colCount ); - } - - if ( maxCols == 0 ) { - mMinIntrinsicWidth = mMaxIntrinsicWidth = mPaddingPx.Left + mPaddingPx.Right; - mIntrinsicWidthsDirty = false; - return; - } - - me->mColMinWidths.assign( maxCols, 0.f ); - me->mColMaxWidths.assign( maxCols, 0.f ); - me->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 (simple even distribution for now) - 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 + mPaddingPx.Left + mPaddingPx.Right + ( maxCols + 1 ) * mCellspacing; - mMaxIntrinsicWidth = - totalMax + mPaddingPx.Left + mPaddingPx.Right + ( maxCols + 1 ) * mCellspacing; - - mIntrinsicWidthsDirty = false; + UILayouter* layouter = const_cast( this )->getLayouter(); + if ( layouter ) + layouter->computeIntrinsicWidths(); } Float UIHTMLTable::getMinIntrinsicWidth() const { computeIntrinsicWidths(); - return mMinIntrinsicWidth; + UILayouter* layouter = const_cast( this )->getLayouter(); + if ( layouter ) + return static_cast( layouter )->getMinIntrinsicWidth(); + return 0; } Float UIHTMLTable::getMaxIntrinsicWidth() const { computeIntrinsicWidths(); - return mMaxIntrinsicWidth; + UILayouter* layouter = const_cast( this )->getLayouter(); + if ( layouter ) + return static_cast( layouter )->getMaxIntrinsicWidth(); + return 0; } void UIHTMLTable::updateLayout() { - if ( mPacking || !mVisible ) - return; - mPacking = true; - setMatchParentIfNeededVerticalGrowth(); + UILayouter* layouter = const_cast( this )->getLayouter(); + if ( layouter ) + getLayouter()->updateLayout(); + else + UIHTMLWidget::updateLayout(); - const StyleSheetProperty* prop = nullptr; - if ( getLayoutWidthPolicy() == SizePolicy::Fixed && mStyle && - ( prop = mStyle->getProperty( PropertyId::Width ) ) ) { - setInternalPixelsSize( { lengthFromValue( *prop ), mSize.getHeight() } ); - } - - computeIntrinsicWidths(); - - if ( mRows.empty() ) { - mPacking = false; - return; - } - - size_t maxCols = mColMinWidths.size(); - mColWidths.assign( maxCols, 0.f ); - Float paddingH = mPaddingPx.Left + mPaddingPx.Right; - Float containerWidth = 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; // Make sure this is uncommented - for ( size_t i = 0; i < maxCols; ++i ) { - totalMin += sanitizeFloat( mColMinWidths[i] ); - totalMax += sanitizeFloat( mColMaxWidths[i] ); // Accumulate max widths - } - - Float tableUsedWidth = availableWidth; // always try to fill the container - - // 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( 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 ) { - // 1. Too narrow → scale down proportionally to min widths - 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 ) { - // 2. Partial flex → space is between min and max. Distribute extra by flexibility (text - // wrapping) - 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; // explicit widths stay rigid here - 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 { - // Fallback if no flex exists - Float scale = totalMin > 0.001f ? ( tableUsedWidth / totalMin ) : 0.f; - for ( size_t i = 0; i < maxCols; ++i ) - mColWidths[i] = mColMinWidths[i] * scale; - } - - } else { - // 3. Abundant space → table is wider than all max widths combined. - // Give everyone their max width, then distribute the leftover space. - 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 ) { - // Distribute leftover space proportionally to max-widths for a balanced look - 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]; // Rigid explicit column stays rigid - } - } - } else { - // Fallback to strict even split if max widths are 0 - Float share = leftOver / static_cast( 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 { - // Absolute fallback: All columns explicitly specified, but space remains. Scale up. - Float scale = totalMax > 0.001f ? ( tableUsedWidth / totalMax ) : 0.f; - for ( size_t i = 0; i < maxCols; ++i ) - mColWidths[i] = mColMaxWidths[i] * scale; - } - } - - // Safety fallback (should never trigger now) - Float sum = 0.f; - for ( float w : mColWidths ) - sum += w; - if ( sum < 1.f && maxCols > 0 ) { - Float w = tableUsedWidth / static_cast( 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; - - // Apply layout and calculate heights - 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; - } - - // Position cells inside the row and equalize height - 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; - } - - // Set row height and width - 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; - } - } - - // Position rows vertically - if ( mHead ) { - mHead->setPixelsPosition( 0, 0 ); - mHead->setPixelsSize( { getPixelsSize().x, headHeight } ); - } - - if ( mBody ) { - mBody->setPixelsPosition( 0, headHeight ); - mBody->setPixelsSize( { getPixelsSize().x, bodyHeight } ); - } - - if ( mFooter ) { - mFooter->setPixelsPosition( 0, headHeight + bodyHeight ); - mFooter->setPixelsSize( { getPixelsSize().x, footerHeight } ); - } - - Float currentY = mPaddingPx.Top + mCellspacing - headHeight; - for ( size_t r = 0; r < rowCount; ++r ) { - UIHTMLTableRow* row = mRows[r]; - row->setPixelsPosition( mPaddingPx.Left, currentY ); - currentY += row->getPixelsSize().getHeight() + mCellspacing; - } - - // Reset positions if they are inside specialized containers - if ( mHead && !mRows.empty() ) - mRows[0]->setPixelsPosition( mPaddingPx.Left, 0 ); - - if ( mFooter && !mRows.empty() ) - mRows[rowCount - 1]->setPixelsPosition( mPaddingPx.Left, 0 ); - - if ( mHeightPolicy == SizePolicy::WrapContent ) { - setInternalPixelsHeight( mPaddingPx.Top + headHeight + bodyHeight + footerHeight + - ( rowCount + 1 ) * mCellspacing + mPaddingPx.Bottom ); - } - - mPacking = false; mDirtyLayout = false; } Uint32 UIHTMLTable::onMessage( const NodeMessage* Msg ) { switch ( Msg->getMsg() ) { case NodeMessage::LayoutAttributeChange: { - if ( Msg->getSender() != this && !mPacking ) { - mIntrinsicWidthsDirty = true; + if ( Msg->getSender() != this && !isPacking() ) { + if ( getLayouter() ) + getLayouter()->invalidateIntrinsicWidths(); notifyLayoutAttrChangeParent(); } tryUpdateLayout(); @@ -597,7 +115,8 @@ UIHTMLTableRow* UIHTMLTableRow::New() { return eeNew( UIHTMLTableRow, () ); } -UIHTMLTableRow::UIHTMLTableRow() : UIWidget( "tr" ) { +UIHTMLTableRow::UIHTMLTableRow() : UIHTMLWidget( "tr" ) { + mDisplay = CSSDisplay::TableRow; mWidthPolicy = SizePolicy::MatchParent; mHeightPolicy = SizePolicy::WrapContent; } @@ -607,7 +126,7 @@ Uint32 UIHTMLTableRow::getType() const { } bool UIHTMLTableRow::isType( const Uint32& type ) const { - return UIHTMLTableRow::getType() == type || UIWidget::isType( type ); + return UIHTMLTableRow::getType() == type || UIHTMLWidget::isType( type ); } UIHTMLTableCell* UIHTMLTableCell::New( const std::string& tag ) { @@ -615,6 +134,7 @@ UIHTMLTableCell* UIHTMLTableCell::New( const std::string& tag ) { } UIHTMLTableCell::UIHTMLTableCell( const std::string& tag ) : UIRichText( tag ) { + mDisplay = CSSDisplay::TableCell; mWidthPolicy = SizePolicy::WrapContent; mHeightPolicy = SizePolicy::WrapContent; } @@ -657,7 +177,8 @@ UIHTMLTableHead* UIHTMLTableHead::New() { return eeNew( UIHTMLTableHead, () ); } -UIHTMLTableHead::UIHTMLTableHead() : UIWidget( "thead" ) { +UIHTMLTableHead::UIHTMLTableHead() : UIHTMLWidget( "thead" ) { + mDisplay = CSSDisplay::TableHead; mWidthPolicy = SizePolicy::MatchParent; mHeightPolicy = SizePolicy::WrapContent; } @@ -667,14 +188,15 @@ Uint32 UIHTMLTableHead::getType() const { } bool UIHTMLTableHead::isType( const Uint32& type ) const { - return UIHTMLTableHead::getType() == type || UIWidget::isType( type ); + return UIHTMLTableHead::getType() == type || UIHTMLWidget::isType( type ); } UIHTMLTableBody* UIHTMLTableBody::New() { return eeNew( UIHTMLTableBody, () ); } -UIHTMLTableBody::UIHTMLTableBody() : UIWidget( "tbody" ) { +UIHTMLTableBody::UIHTMLTableBody() : UIHTMLWidget( "tbody" ) { + mDisplay = CSSDisplay::TableBody; mWidthPolicy = SizePolicy::MatchParent; mHeightPolicy = SizePolicy::WrapContent; } @@ -684,14 +206,15 @@ Uint32 UIHTMLTableBody::getType() const { } bool UIHTMLTableBody::isType( const Uint32& type ) const { - return UIHTMLTableBody::getType() == type || UIWidget::isType( type ); + return UIHTMLTableBody::getType() == type || UIHTMLWidget::isType( type ); } UIHTMLTableFooter* UIHTMLTableFooter::New() { return eeNew( UIHTMLTableFooter, () ); } -UIHTMLTableFooter::UIHTMLTableFooter() : UIWidget( "tfoot" ) { +UIHTMLTableFooter::UIHTMLTableFooter() : UIHTMLWidget( "tfoot" ) { + mDisplay = CSSDisplay::TableFooter; mWidthPolicy = SizePolicy::MatchParent; mHeightPolicy = SizePolicy::WrapContent; } @@ -701,7 +224,7 @@ Uint32 UIHTMLTableFooter::getType() const { } bool UIHTMLTableFooter::isType( const Uint32& type ) const { - return UIHTMLTableFooter::getType() == type || UIWidget::isType( type ); + return UIHTMLTableFooter::getType() == type || UIHTMLWidget::isType( type ); } }} // namespace EE::UI diff --git a/src/eepp/ui/uihtmlwidget.cpp b/src/eepp/ui/uihtmlwidget.cpp new file mode 100644 index 000000000..2039dedec --- /dev/null +++ b/src/eepp/ui/uihtmlwidget.cpp @@ -0,0 +1,158 @@ +#include +#include +#include +#include + +namespace EE { namespace UI { + +UIHTMLWidget* UIHTMLWidget::New() { + return eeNew( UIHTMLWidget, () ); +} + +UIHTMLWidget::UIHTMLWidget( const std::string& tag ) : UILayout( tag ) {} + +UIHTMLWidget::~UIHTMLWidget() { + eeSAFE_DELETE( mLayouter ); +} + +UILayouter* UIHTMLWidget::getLayouter() { + if ( !mLayouter ) { + mLayouter = UILayouterManager::create( mDisplay, this ); + } + return mLayouter; +} + +Uint32 UIHTMLWidget::getType() const { + return UI_TYPE_HTML_WIDGET; +} + +bool UIHTMLWidget::isType( const Uint32& type ) const { + return UIHTMLWidget::getType() == type ? true : UILayout::isType( type ); +} + +bool UIHTMLWidget::isPacking() const { + UILayouter* layouter = const_cast( this )->getLayouter(); + if ( layouter ) + return layouter->isPacking(); + return UILayout::isPacking(); +} + +void UIHTMLWidget::onDisplayChange() { + eeSAFE_DELETE( mLayouter ); + getLayouter(); + notifyLayoutAttrChange(); +} + +void UIHTMLWidget::setDisplay( CSSDisplay display ) { + if ( mDisplay != display ) { + mDisplay = display; + onDisplayChange(); + } +} + +void UIHTMLWidget::setCSSPosition( CSSPosition position ) { + if ( mPosition != position ) { + mPosition = position; + onPositionChange(); + } +} + +void UIHTMLWidget::setOffsets( const Rectf& offsets ) { + if ( mOffsets != offsets ) { + mOffsets = offsets; + notifyLayoutAttrChange(); + } +} + +void UIHTMLWidget::setZIndex( int zIndex ) { + mZIndex = zIndex; +} + +std::string UIHTMLWidget::getPropertyString( const PropertyDefinition* propertyDef, + const Uint32& state ) const { + if ( NULL == propertyDef ) + return ""; + + switch ( propertyDef->getPropertyId() ) { + case PropertyId::Display: + return CSSDisplayHelper::toString( mDisplay ); + case PropertyId::Position: + return CSSPositionHelper::toString( mPosition ); + case PropertyId::Top: + return String::fromFloat( mOffsets.Top, "dp" ); + case PropertyId::Right: + return String::fromFloat( mOffsets.Right, "dp" ); + case PropertyId::Bottom: + return String::fromFloat( mOffsets.Bottom, "dp" ); + case PropertyId::Left: + return String::fromFloat( mOffsets.Left, "dp" ); + case PropertyId::ZIndex: + return String::toString( mZIndex ); + default: + return UILayout::getPropertyString( propertyDef ); + } +} + +bool UIHTMLWidget::applyProperty( const StyleSheetProperty& attribute ) { + if ( !checkPropertyDefinition( attribute ) ) + return false; + + switch ( attribute.getPropertyDefinition()->getPropertyId() ) { + case PropertyId::Display: { + setDisplay( CSSDisplayHelper::fromString( attribute.asString() ) ); + return true; + } + case PropertyId::Position: { + setCSSPosition( CSSPositionHelper::fromString( attribute.asString() ) ); + return true; + } + case PropertyId::ZIndex: { + setZIndex( attribute.asInt() ); + return true; + } + case PropertyId::Top: { + if ( attribute.asString() == "auto" ) + mOffsets.Top = 0; + else + mOffsets.Top = lengthFromValueAsDp( attribute ); + notifyLayoutAttrChange(); + return true; + } + case PropertyId::Right: { + if ( attribute.asString() == "auto" ) + mOffsets.Right = 0; + else + mOffsets.Right = lengthFromValueAsDp( attribute ); + notifyLayoutAttrChange(); + return true; + } + case PropertyId::Bottom: { + if ( attribute.asString() == "auto" ) + mOffsets.Bottom = 0; + else + mOffsets.Bottom = lengthFromValueAsDp( attribute ); + notifyLayoutAttrChange(); + return true; + } + case PropertyId::Left: { + if ( attribute.asString() == "auto" ) + mOffsets.Left = 0; + else + mOffsets.Left = lengthFromValueAsDp( attribute ); + notifyLayoutAttrChange(); + return true; + } + default: + break; + } + + return UILayout::applyProperty( attribute ); +} + +void UIHTMLWidget::invalidateIntrinsicSize() { + if ( mLayouter ) + mLayouter->invalidateIntrinsicWidths(); + UIWidget::invalidateIntrinsicSize(); +} + +}} // namespace EE::UI diff --git a/src/eepp/ui/uilayouter.cpp b/src/eepp/ui/uilayouter.cpp new file mode 100644 index 000000000..b4d5a8b77 --- /dev/null +++ b/src/eepp/ui/uilayouter.cpp @@ -0,0 +1,10 @@ +#include +#include + +namespace EE { namespace UI { + +void UILayouter::setMatchParentIfNeededVerticalGrowth() { + mContainer->asType()->setMatchParentIfNeededVerticalGrowth(); +} + +}} // namespace EE::UI diff --git a/src/eepp/ui/uilayoutermanager.cpp b/src/eepp/ui/uilayoutermanager.cpp new file mode 100644 index 000000000..21b73c209 --- /dev/null +++ b/src/eepp/ui/uilayoutermanager.cpp @@ -0,0 +1,28 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace EE { namespace UI { + +UILayouter* UILayouterManager::create( CSSDisplay display, UIWidget* container ) { + switch ( display ) { + case CSSDisplay::Block: + case CSSDisplay::TableCell: + return eeNew( BlockLayouter, ( container ) ); + case CSSDisplay::Inline: + case CSSDisplay::InlineBlock: + return eeNew( InlineLayouter, ( container ) ); + case CSSDisplay::Table: + return eeNew( TableLayouter, ( container ) ); + case CSSDisplay::None: + return eeNew( NoneLayouter, ( container ) ); + default: + return nullptr; + } +} + +}} // namespace EE::UI diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index c3709f41e..842fc59e4 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -15,19 +16,6 @@ namespace EE { namespace UI { -class UILineBreak : public UIRichText { - public: - static UILineBreak* New( const std::string& tag ) { return eeNew( UILineBreak, ( tag ) ); } - - UILineBreak( const std::string& tag = "br" ) : UIRichText( tag ) {} - - virtual Uint32 getType() const { return UI_TYPE_BR; } - - bool isType( const Uint32& type ) const { - return UILineBreak::getType() == type ? true : UINode::isType( type ); - } -}; - UIHTMLHtml* UIHTMLHtml::New( const std::string& tag ) { return eeNew( UIHTMLHtml, ( tag ) ); } @@ -42,6 +30,20 @@ bool UIHTMLHtml::isType( const Uint32& type ) const { return UIHTMLHtml::getType() == type ? true : UIRichText::isType( type ); } +UILineBreak* UILineBreak::New( const std::string& tag ) { + return eeNew( UILineBreak, ( tag ) ); +} + +UILineBreak::UILineBreak( const std::string& tag ) : UIRichText( tag ) {} + +Uint32 UILineBreak::getType() const { + return UI_TYPE_BR; +} + +bool UILineBreak::isType( const Uint32& type ) const { + return UILineBreak::getType() == type ? true : UIHTMLWidget::isType( type ); +} + UIHTMLBody* UIHTMLBody::New( const std::string& tag ) { return eeNew( UIHTMLBody, ( tag ) ); } @@ -110,7 +112,7 @@ UIRichText* UIRichText::NewWithTag( const std::string& tag ) { return eeNew( UIRichText, ( tag ) ); } -UIRichText::UIRichText( const std::string& tag ) : UILayout( tag ) { +UIRichText::UIRichText( const std::string& tag ) : UIHTMLWidget( tag ) { mFlags |= UI_HTML_ELEMENT | UI_LOADS_ITS_CHILDREN | UI_OWNS_CHILDREN_POSITION; UITheme* theme = getUISceneNode()->getUIThemeManager()->getDefaultTheme(); @@ -137,7 +139,7 @@ Uint32 UIRichText::getType() const { } bool UIRichText::isType( const Uint32& type ) const { - return UIRichText::getType() == type ? true : UILayout::isType( type ); + return UIRichText::getType() == type ? true : UIHTMLWidget::isType( type ); } const RichText& UIRichText::getRichText() { @@ -542,22 +544,23 @@ void UIRichText::onAlphaChange() { UILayout::onAlphaChange(); } -void UIRichText::rebuildRichText( RichText& richText, IntrinsicMode mode ) { +void UIRichText::rebuildRichText( UILayout* container, RichText& richText, IntrinsicMode mode ) { richText.clear(); - - Float maxWidth = mSize.getWidth() - mPaddingPx.Left - mPaddingPx.Right; + Float maxWidth = container->getPixelsSize().getWidth() - container->getPixelsPadding().Left - + container->getPixelsPadding().Right; if ( maxWidth < 0 ) maxWidth = 0; Float mw = 0.f; - if ( !mMaxWidthEq.empty() ) { - mw = getMaxSizePx().getWidth() - mPaddingPx.Left - mPaddingPx.Right; + if ( !container->getMaxWidthEq().empty() ) { + mw = container->getMaxSizePx().getWidth() - container->getPixelsPadding().Left - + container->getPixelsPadding().Right; if ( mw < 0 ) mw = 0.f; } if ( mode == IntrinsicMode::None ) { - if ( !mMaxWidthEq.empty() && ( maxWidth == 0 || mw < maxWidth ) ) { + if ( !container->getMaxWidthEq().empty() && ( maxWidth == 0 || mw < maxWidth ) ) { richText.setMaxWidth( mw ); } else { richText.setMaxWidth( maxWidth ); @@ -588,18 +591,19 @@ void UIRichText::rebuildRichText( RichText& richText, IntrinsicMode mode ) { if ( mode == IntrinsicMode::None ) { if ( isBlock ) { - if ( mSize.getWidth() != 0 ) { - Float maxSize = - eemax( 0.f, mSize.getWidth() - mPaddingPx.Left - mPaddingPx.Right - - margin.Left - margin.Right ); + if ( container->getPixelsSize().getWidth() != 0 ) { + Float maxSize = eemax( 0.f, container->getPixelsSize().getWidth() - + container->getPixelsPadding().Left - + container->getPixelsPadding().Right - + margin.Left - margin.Right ); widget->setPixelsSize( eemax( 0.f, maxSize ), widget->getPixelsSize().getHeight() ); } else { - onAutoSizeChild( widget ); + container->onAutoSizeChild( widget ); } } else if ( widget->getLayoutWidthPolicy() == SizePolicy::WrapContent || widget->getLayoutHeightPolicy() == SizePolicy::WrapContent ) { - onAutoSizeChild( widget ); + container->onAutoSizeChild( widget ); } } @@ -613,9 +617,12 @@ void UIRichText::rebuildRichText( RichText& richText, IntrinsicMode mode ) { } Float w = size.getWidth(); - if ( isBlock && mode == IntrinsicMode::None && mSize.getWidth() != 0 ) { - w = eemax( 0.f, mSize.getWidth() - mPaddingPx.Left - mPaddingPx.Right - - margin.Left - margin.Right ); + if ( isBlock && mode == IntrinsicMode::None && + container->getPixelsSize().getWidth() != 0 ) { + w = eemax( 0.f, container->getPixelsSize().getWidth() - + container->getPixelsPadding().Left - + container->getPixelsPadding().Right - margin.Left - + margin.Right ); } richText.addCustomSize( Sizef( w + margin.Left + margin.Right, @@ -624,7 +631,7 @@ void UIRichText::rebuildRichText( RichText& richText, IntrinsicMode mode ) { } }; - Node* child = mChild; + Node* child = container->getFirstChild(); while ( NULL != child ) { if ( child->isWidget() ) { processWidget( child->asType(), processWidget ); @@ -633,140 +640,8 @@ void UIRichText::rebuildRichText( RichText& richText, IntrinsicMode mode ) { } } -void UIRichText::positionChildren() { - const auto& lines = mRichText.getLines(); - Node* child = mChild; - - size_t currentLine = 0; - size_t currentSpan = 0; - - // Helper to find the next RenderSpan of type CustomSize - 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( span.block ) ) - return &span; - } - currentSpan = 0; - currentLine++; - } - return nullptr; - }; - - Int64 curCharIdx = 0; - - auto processWidget = [&]( UIWidget* widget, auto& processWidgetRef ) -> Rectf { - constexpr Float maxF = std::numeric_limits::max(); - constexpr Float lowF = std::numeric_limits::lowest(); - Rectf bounds( maxF, maxF, lowF, lowF ); - - Vector2f offset; - Node* p = widget->getParent(); - while ( p && p != this ) { - offset += p->isWidget() ? p->asType()->getPixelsPosition() : p->getPosition(); - p = p->getParent(); - } - - if ( widget->isType( UI_TYPE_TEXTSPAN ) ) { - UITextSpan* textSpan = widget->asType(); - 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( mPaddingPx.Left + rspan.position.x, - mPaddingPx.Top + line.y + rspan.position.y, - mPaddingPx.Left + rspan.position.x + rspan.size.getWidth(), - mPaddingPx.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; - } - } - - Node* spanChild = widget->getFirstChild(); - while ( spanChild != NULL ) { - if ( spanChild->isWidget() ) { - bounds.expand( - processWidgetRef( spanChild->asType(), processWidgetRef ) ); - } - spanChild = spanChild->getNextNode(); - } - - // Ensure the parent span at least has enough size to cover its children - 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()->getPixelsPosition(); - pos.y += widget->getPrevNode()->getPixelsSize().getHeight(); - } - widget->setPixelsPosition( pos ); - widget->setPixelsSize( - { eemax( 0.f, mSize.getWidth() - mPaddingPx.Left - mPaddingPx.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( mPaddingPx.Left + span->position.x + margin.Left, - mPaddingPx.Top + lineY + span->position.y + margin.Top ); - - widget->setPixelsPosition( targetPos - offset ); - - bounds = Rectf( targetPos, span->size ); - } - } - return bounds; - }; - - child = mChild; - while ( NULL != child ) { - if ( child->isWidget() ) { - processWidget( child->asType(), processWidget ); - } - child = child->getNextNode(); - } +void UIRichText::rebuildRichText( RichText& richText, IntrinsicMode mode ) { + rebuildRichText( this, richText, mode ); } void UIRichText::updateDefaultSpansStyle() { @@ -780,51 +655,13 @@ void UIRichText::updateDefaultSpansStyle() { } void UIRichText::updateLayout() { - if ( mPacking ) - return; - mResizedCount = 0; - mPacking = true; - - setMatchParentIfNeededVerticalGrowth(); - - const StyleSheetProperty* prop = nullptr; - if ( getLayoutWidthPolicy() == SizePolicy::Fixed && mStyle && - ( prop = mStyle->getProperty( PropertyId::Width ) ) ) { - setInternalPixelsSize( { lengthFromValue( *prop ), mSize.getHeight() } ); + if ( getLayouter() ) { + getLayouter()->updateLayout(); + } else { + UILayout::updateLayout(); } - rebuildRichText( mRichText ); - - mRichText.updateLayout(); - - positionChildren(); - - Float totW = mSize.getWidth(); - if ( mWidthPolicy == SizePolicy::WrapContent ) { - totW = mRichText.getSize().getWidth() + mPaddingPx.Left + mPaddingPx.Right; - if ( !mMaxWidthEq.empty() && totW > getMaxSizePx().getWidth() ) - setClipType( ClipType::ContentBox ); - } - - if ( totW != mSize.getWidth() || mWidthPolicy == SizePolicy::WrapContent ) - setInternalPixelsWidth( totW ); - - Float totH = mSize.getHeight(); - if ( mHeightPolicy == SizePolicy::WrapContent ) { - totH = mRichText.getSize().getHeight() + mPaddingPx.Top + mPaddingPx.Bottom; - if ( !mMaxHeightEq.empty() && totH > getMaxSizePx().getHeight() ) - setClipType( ClipType::ContentBox ); - } - - if ( totH != mSize.getHeight() || mHeightPolicy == SizePolicy::WrapContent ) - setInternalPixelsHeight( totH ); - - if ( mResizedCount ) - positionChildren(); - - mPacking = false; mDirtyLayout = false; - mResizedCount = 0; } Float UIRichText::getMinIntrinsicWidth() const { @@ -832,11 +669,18 @@ Float UIRichText::getMinIntrinsicWidth() const { return getPropertyWidth(); } - if ( mIntrinsicWidthsDirty ) { + UILayouter* layouter = const_cast( this )->getLayouter(); + if ( mIntrinsicWidthsDirty && layouter ) { + layouter->computeIntrinsicWidths(); + mMinIntrinsicWidth = layouter->getMinIntrinsicWidth(); + mMaxIntrinsicWidth = layouter->getMaxIntrinsicWidth(); + } else if ( mIntrinsicWidthsDirty ) { RichText richText( mRichText ); - const_cast( this )->rebuildRichText( richText, IntrinsicMode::Min ); + UIRichText::rebuildRichText( const_cast( this ), richText, + IntrinsicMode::Min ); mMinIntrinsicWidth = richText.getMinIntrinsicWidth() + mPaddingPx.Left + mPaddingPx.Right; - const_cast( this )->rebuildRichText( richText, IntrinsicMode::Max ); + UIRichText::rebuildRichText( const_cast( this ), richText, + IntrinsicMode::Max ); mMaxIntrinsicWidth = richText.getMaxIntrinsicWidth() + mPaddingPx.Left + mPaddingPx.Right; mIntrinsicWidthsDirty = false; } @@ -854,16 +698,24 @@ Float UIRichText::getMaxIntrinsicWidth() const { return getPropertyWidth(); } - if ( mIntrinsicWidthsDirty ) { - RichText richText( mRichText ); - const_cast( this )->rebuildRichText( richText, IntrinsicMode::Min ); - mMinIntrinsicWidth = richText.getMinIntrinsicWidth() + mPaddingPx.Left + mPaddingPx.Right; - const_cast( this )->rebuildRichText( richText, IntrinsicMode::Max ); - mMaxIntrinsicWidth = richText.getMaxIntrinsicWidth() + mPaddingPx.Left + mPaddingPx.Right; - mIntrinsicWidthsDirty = false; + Float maxW = 0; + if ( const_cast( this )->getLayouter() ) { + maxW = const_cast( this )->getLayouter()->getMaxIntrinsicWidth(); + } else { + if ( mIntrinsicWidthsDirty ) { + RichText richText( mRichText ); + const_cast( this )->rebuildRichText( richText, IntrinsicMode::Min ); + mMinIntrinsicWidth = + richText.getMinIntrinsicWidth() + mPaddingPx.Left + mPaddingPx.Right; + const_cast( this )->rebuildRichText( richText, IntrinsicMode::Max ); + mMaxIntrinsicWidth = + richText.getMaxIntrinsicWidth() + mPaddingPx.Left + mPaddingPx.Right; + mIntrinsicWidthsDirty = false; + } + maxW = mMaxIntrinsicWidth; } - Float maxWidth = mMaxIntrinsicWidth; + Float maxWidth = maxW; if ( !mMinWidthEq.empty() ) maxWidth = eemax( maxWidth, getMinSizePx().getWidth() ); if ( !mMaxWidthEq.empty() ) @@ -875,7 +727,7 @@ Uint32 UIRichText::onMessage( const NodeMessage* Msg ) { switch ( Msg->getMsg() ) { case NodeMessage::LayoutAttributeChange: { if ( Msg->getSender() != this && !mPacking ) { - mIntrinsicWidthsDirty = true; + invalidateIntrinsicSize(); notifyLayoutAttrChangeParent(); } tryUpdateLayout(); diff --git a/src/eepp/ui/uitextspan.cpp b/src/eepp/ui/uitextspan.cpp index 0f5933016..15df5f661 100644 --- a/src/eepp/ui/uitextspan.cpp +++ b/src/eepp/ui/uitextspan.cpp @@ -20,7 +20,8 @@ UITextSpan* UITextSpan::NewWithTag( const std::string& tag ) { return eeNew( UITextSpan, ( tag ) ); } -UITextSpan::UITextSpan( const std::string& tag ) : UIWidget( tag ) { +UITextSpan::UITextSpan( const std::string& tag ) : UIHTMLWidget( tag ) { + mDisplay = CSSDisplay::Inline; mFlags |= UI_HTML_ELEMENT | UI_VALIGN_CENTER | UI_HALIGN_LEFT | UI_LOADS_ITS_CHILDREN; UITheme* theme = getUISceneNode()->getUIThemeManager()->getDefaultTheme(); From 8a9bc2cd474ca1724637cea7d92a5b6585ddb98e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sat, 25 Apr 2026 20:47:51 -0300 Subject: [PATCH 2/2] Some HTML rendering fixes. Most important is the media query is correctly processed when combining style sheets. --- bin/assets/ui/breeze.css | 4 ++++ include/eepp/ui/css/propertydefinition.hpp | 1 + include/eepp/ui/uipushbutton.hpp | 2 ++ include/eepp/ui/uitextspan.hpp | 2 ++ src/eepp/ui/css/stylesheetspecification.cpp | 15 ++++++++------- src/eepp/ui/uinode.cpp | 5 +++++ src/eepp/ui/uipushbutton.cpp | 9 +++++++++ src/eepp/ui/uirichtext.cpp | 4 +++- src/eepp/ui/uiscenenode.cpp | 15 +++++++++++++-- src/eepp/ui/uitextview.cpp | 10 ++++++---- src/eepp/ui/uiwidget.cpp | 3 +++ src/eepp/ui/uiwidgetcreator.cpp | 8 +++++++- src/tools/ecode/uiwelcomescreen.cpp | 16 ++++++++-------- 13 files changed, 71 insertions(+), 23 deletions(-) diff --git a/bin/assets/ui/breeze.css b/bin/assets/ui/breeze.css index f10fd3e86..48c6b0385 100644 --- a/bin/assets/ui/breeze.css +++ b/bin/assets/ui/breeze.css @@ -62,6 +62,10 @@ strong { font-style: bold; } +small { + font-size: smaller; +} + u, ins { text-decoration: underline; diff --git a/include/eepp/ui/css/propertydefinition.hpp b/include/eepp/ui/css/propertydefinition.hpp index 2b212b97e..4525ef866 100644 --- a/include/eepp/ui/css/propertydefinition.hpp +++ b/include/eepp/ui/css/propertydefinition.hpp @@ -239,6 +239,7 @@ enum class PropertyId : Uint32 { 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" ), diff --git a/include/eepp/ui/uipushbutton.hpp b/include/eepp/ui/uipushbutton.hpp index 877bc2bca..87e81276d 100644 --- a/include/eepp/ui/uipushbutton.hpp +++ b/include/eepp/ui/uipushbutton.hpp @@ -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; diff --git a/include/eepp/ui/uitextspan.hpp b/include/eepp/ui/uitextspan.hpp index 9583a939e..d690e9f05 100644 --- a/include/eepp/ui/uitextspan.hpp +++ b/include/eepp/ui/uitextspan.hpp @@ -33,6 +33,8 @@ class EE_API UITextSpan : public UIHTMLWidget { static UITextSpan* NewCode() { return NewWithTag( "code" ); } + static UITextSpan* NewSmall() { return NewWithTag( "small" ); } + virtual ~UITextSpan(); virtual Uint32 getType() const; diff --git a/src/eepp/ui/css/stylesheetspecification.cpp b/src/eepp/ui/css/stylesheetspecification.cpp index 77dbc94dd..378b01d54 100644 --- a/src/eepp/ui/css/stylesheetspecification.cpp +++ b/src/eepp/ui/css/stylesheetspecification.cpp @@ -425,13 +425,14 @@ void StyleSheetSpecification::registerDefaultProperties() { registerProperty( "cols", "20" ).setType( PropertyType::NumberInt ); registerProperty( "input-mode", "normal" ).setType( PropertyType::String ); - registerProperty( "display", "inline" ); - registerProperty( "position", "static" ); - registerProperty( "top", "auto" ); - registerProperty( "right", "auto" ); - registerProperty( "bottom", "auto" ); - registerProperty( "left", "auto" ); - registerProperty( "z-index", "auto" ); + registerProperty( "hidden", "" ).setType( PropertyType::Bool ); + registerProperty( "display", "inline" ).setType( PropertyType::String ); + registerProperty( "position", "static" ).setType( PropertyType::String ); + registerProperty( "top", "auto" ).setType( PropertyType::NumberLength ); + registerProperty( "right", "auto" ).setType( PropertyType::NumberLength ); + registerProperty( "bottom", "auto" ).setType( PropertyType::NumberLength ); + registerProperty( "left", "auto" ).setType( PropertyType::NumberLength ); + registerProperty( "z-index", "auto" ).setType( PropertyType::NumberInt ); registerProperty( "inner-widget-orientation", "widgeticontextbox" ) .setType( PropertyType::String ); diff --git a/src/eepp/ui/uinode.cpp b/src/eepp/ui/uinode.cpp index 574dc7bee..4ce1ec627 100644 --- a/src/eepp/ui/uinode.cpp +++ b/src/eepp/ui/uinode.cpp @@ -1670,6 +1670,11 @@ Float UINode::lengthFromValue( const StyleSheetProperty& property, res.setValue( 1.2f, StyleSheetLength::Unit::Em ); } return convertLength( res, 0 ); + } else if ( property.getValue() == "inherit" ) { + // TODO: FIX inherit value + StyleSheetLength res; + res.setValue( 1, StyleSheetLength::Unit::Em ); + return convertLength( res, 0 ); } } return lengthFromValue( property.getValue(), diff --git a/src/eepp/ui/uipushbutton.cpp b/src/eepp/ui/uipushbutton.cpp index 173cd0002..0f5efafb9 100644 --- a/src/eepp/ui/uipushbutton.cpp +++ b/src/eepp/ui/uipushbutton.cpp @@ -6,6 +6,9 @@ #include #include +#define PUGIXML_HEADER_ONLY +#include + namespace EE { namespace UI { InnerWidgetOrientation UIPushButton::innerWidgetOrientationFromString( std::string iwo ) { @@ -739,4 +742,10 @@ bool UIPushButton::applyProperty( const StyleSheetProperty& attribute ) { return attributeSet; } +void UIPushButton::loadFromXmlNode( const pugi::xml_node& node ) { + UIWidget::loadFromXmlNode( node ); + if ( !node.text().empty() ) + setText( node.text().as_string() ); +} + }} // namespace EE::UI diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index 842fc59e4..08108ef1b 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -20,7 +20,9 @@ UIHTMLHtml* UIHTMLHtml::New( const std::string& tag ) { return eeNew( UIHTMLHtml, ( tag ) ); } -UIHTMLHtml::UIHTMLHtml( const std::string& tag ) : UIRichText( tag ) {} +UIHTMLHtml::UIHTMLHtml( const std::string& tag ) : UIRichText( tag ) { + enableReportSizeChangeToChildren(); +} Uint32 UIHTMLHtml::getType() const { return UI_TYPE_HTML_HTML; diff --git a/src/eepp/ui/uiscenenode.cpp b/src/eepp/ui/uiscenenode.cpp index 3aa0f749a..e643fe94e 100644 --- a/src/eepp/ui/uiscenenode.cpp +++ b/src/eepp/ui/uiscenenode.cpp @@ -400,10 +400,21 @@ void UISceneNode::setStyleSheet( const std::string& inlineStyleSheet ) { void UISceneNode::combineStyleSheet( const CSS::StyleSheet& styleSheet, bool forceReloadStyle, URI baseURI ) { mStyleSheet.combineStyleSheet( styleSheet ); + + bool mediaChanged = false; + if ( !mStyleSheet.isMediaQueryListEmpty() && + mStyleSheet.updateMediaLists( getMediaFeatures() ) ) { + mediaChanged = true; + } + + processStyleSheetAtRules( styleSheet, baseURI ); + if ( mRoot && mRoot->getUIStyle() ) mRoot->getUIStyle()->resetGlobalDefinition(); - processStyleSheetAtRules( styleSheet, baseURI ); - onMediaChanged(); + + if ( mRoot && mediaChanged ) + mRoot->reportStyleStateChangeRecursive( false, false ); + if ( forceReloadStyle ) reloadStyle(); } diff --git a/src/eepp/ui/uitextview.cpp b/src/eepp/ui/uitextview.cpp index 1463191a4..38f177ed8 100644 --- a/src/eepp/ui/uitextview.cpp +++ b/src/eepp/ui/uitextview.cpp @@ -742,15 +742,17 @@ bool UITextView::applyProperty( const StyleSheetProperty& attribute ) { setSelectionBackColor( attribute.asColor() ); break; case PropertyId::FontFamily: { - Font* font = FontManager::instance()->getByName( attribute.value() ); + if ( attribute.value() != "inherit" ) { + Font* font = FontManager::instance()->getByName( attribute.value() ); - if ( !mUsingCustomStyling && NULL != font && font->loaded() ) { - setFont( font ); + if ( !mUsingCustomStyling && NULL != font && font->loaded() ) { + setFont( font ); + } } break; } case PropertyId::FontSize: - if ( !mUsingCustomStyling ) + if ( !mUsingCustomStyling && attribute.value() != "inherit" ) setFontSize( lengthFromValue( attribute ) ); break; case PropertyId::TextDecoration: diff --git a/src/eepp/ui/uiwidget.cpp b/src/eepp/ui/uiwidget.cpp index 6a6a6054f..11c6a7f0a 100644 --- a/src/eepp/ui/uiwidget.cpp +++ b/src/eepp/ui/uiwidget.cpp @@ -2248,6 +2248,9 @@ bool UIWidget::applyProperty( const StyleSheetProperty& attribute ) { unsetFlags( UI_TAB_FOCUSABLE ); } break; + case PropertyId::Hidden: + setVisible( false ); + break; default: attributeSet = false; break; diff --git a/src/eepp/ui/uiwidgetcreator.cpp b/src/eepp/ui/uiwidgetcreator.cpp index 160ce7dbb..202c6ba6a 100644 --- a/src/eepp/ui/uiwidgetcreator.cpp +++ b/src/eepp/ui/uiwidgetcreator.cpp @@ -126,7 +126,6 @@ void UIWidgetCreator::createBaseWidgetList() { registeredWidget["hslider"] = UISlider::NewHorizontal; registeredWidget["vscrollbar"] = UIScrollBar::NewVertical; registeredWidget["hscrollbar"] = UIScrollBar::NewHorizontal; - registeredWidget["button"] = UIPushButton::New; registeredWidget["rlay"] = UIRelativeLayout::New; registeredWidget["tooltip"] = UITooltip::New; registeredWidget["tv"] = UITextView::New; @@ -137,6 +136,7 @@ void UIWidgetCreator::createBaseWidgetList() { registeredWidget["em"] = UITextSpan::NewEmphasis; registeredWidget["b"] = UITextSpan::NewBold; registeredWidget["strong"] = UITextSpan::NewBold; + registeredWidget["small"] = UITextSpan::NewSmall; registeredWidget["i"] = UITextSpan::NewItalics; registeredWidget["u"] = UITextSpan::NewUnderline; registeredWidget["ins"] = UITextSpan::NewUnderline; @@ -186,6 +186,12 @@ void UIWidgetCreator::createBaseWidgetList() { registeredWidget["td"] = [] { return UIHTMLTableCell::New( "td" ); }; registeredWidget["input"] = HTMLInput::New; registeredWidget["textarea"] = HTMLTextArea::New; + registeredWidget["button"] = [] { + auto but = UIPushButton::NewWithTag( "button" ); + but->setFlags( UI_HTML_ELEMENT ); + but->setLayoutSizePolicy( SizePolicy::WrapContent, SizePolicy::WrapContent ); + return but; + }; sBaseListCreated = true; } diff --git a/src/tools/ecode/uiwelcomescreen.cpp b/src/tools/ecode/uiwelcomescreen.cpp index b7e357522..fe20b8283 100644 --- a/src/tools/ecode/uiwelcomescreen.cpp +++ b/src/tools/ecode/uiwelcomescreen.cpp @@ -156,14 +156,14 @@ static const auto LAYOUT = R"xml( -