Merge branch 'feature/css-display' into develop

This commit is contained in:
Martín Lucas Golini
2026-04-25 20:48:10 -03:00
36 changed files with 1677 additions and 811 deletions

View File

@@ -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)

View File

@@ -62,6 +62,10 @@ strong {
font-style: bold;
}
small {
font-size: smaller;
}
u,
ins {
text-decoration: underline;

View File

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

View File

@@ -239,6 +239,14 @@ 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" ),
Right = String::hash( "right" ),
Bottom = String::hash( "bottom" ),
Left = String::hash( "left" ),
ZIndex = String::hash( "z-index" ),
};
enum class PropertyType : Uint32 {

View File

@@ -0,0 +1,39 @@
#ifndef EE_UI_CSSLAYOUTTYPES_HPP
#define EE_UI_CSSLAYOUTTYPES_HPP
#include <eepp/config.hpp>
#include <string>
namespace EE { namespace UI {
enum class CSSDisplay {
Inline,
Block,
InlineBlock,
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

View File

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

View File

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

View File

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

View File

@@ -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,

View File

@@ -2,29 +2,18 @@
#define EE_UI_UIHTMLTABLE_HPP
#include <eepp/core/small_vector.hpp>
#include <eepp/ui/uilayout.hpp>
#include <eepp/ui/uihtmlwidget.hpp>
#include <eepp/ui/uirichtext.hpp>
namespace EE { namespace UI {
class UIHTMLTableRow;
class UIHTMLTableCell;
class UIHTMLTableHead;
class UIHTMLTableBody;
class UIHTMLTableFooter;
enum class TableLayout { Auto, Fixed };
class EE_API UIHTMLTable : public UILayout {
class EE_API UIHTMLTable : public UIHTMLWidget {
public:
friend class TableLayouter;
static UIHTMLTable* New();
UIHTMLTable();
void setTableLayout( TableLayout layout );
TableLayout getTableLayout() const;
virtual Uint32 getType() const;
virtual bool isType( const Uint32& type ) const;
@@ -41,25 +30,12 @@ class EE_API UIHTMLTable : public UILayout {
virtual Uint32 onMessage( const NodeMessage* Msg );
void computeIntrinsicWidths() const;
SmallVector<UIHTMLTableRow*> mRows;
SmallVector<Float> mColWidths;
SmallVector<UIHTMLTableCell*> mCells;
SmallVector<Uint32> mRowCellOffsets;
mutable SmallVector<Float> mColMinWidths;
mutable SmallVector<Float> mColMaxWidths;
mutable SmallVector<Float> mColSpecifiedWidths;
TableLayout mTableLayout{ TableLayout::Auto };
mutable UIHTMLTableHead* mHead{ nullptr };
mutable UIHTMLTableBody* mBody{ nullptr };
mutable UIHTMLTableFooter* mFooter{ nullptr };
Float mCellpadding{ 0 };
Float mCellspacing{ 0 };
};
class EE_API UIHTMLTableCell : public UIRichText {
public:
friend class UIHTMLTable;
friend class UIHTMLTable;
friend class TableLayouter;
static UIHTMLTableCell* New( const std::string& tag );
@@ -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();

View File

@@ -0,0 +1,63 @@
#ifndef EE_UI_UIHTMLWIDGET_HPP
#define EE_UI_UIHTMLWIDGET_HPP
#include <eepp/ui/csslayouttypes.hpp>
#include <eepp/ui/uilayout.hpp>
namespace EE { namespace Graphics {
class RichText;
}} // namespace EE::Graphics
namespace EE { namespace UI {
class UILayouter;
class EE_API UIHTMLWidget : public UILayout {
public:
static UIHTMLWidget* New();
UIHTMLWidget( const std::string& tag = "htmlwidget" );
virtual ~UIHTMLWidget();
virtual Uint32 getType() const;
virtual bool isType( const Uint32& type ) const;
UILayouter* getLayouter();
virtual bool isPacking() const;
virtual void onDisplayChange();
CSSDisplay getDisplay() const { return mDisplay; }
void setDisplay( CSSDisplay display );
CSSPosition getCSSPosition() const { return mPosition; }
void setCSSPosition( CSSPosition position );
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

View File

@@ -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<UILayout*> 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

View File

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

View File

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

View File

@@ -32,6 +32,9 @@ class UIWidget;
class EE_API UINode : public Node {
public:
friend class BlockLayouter;
friend class InlineLayouter;
friend class TableLayouter;
/**
* @brief Creates a new UINode instance.
*

View File

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

View File

@@ -2,12 +2,18 @@
#define EE_UI_UIRICHTEXT_HPP
#include <eepp/graphics/richtext.hpp>
#include <eepp/ui/uihtmlwidget.hpp>
#include <eepp/ui/uilayout.hpp>
namespace EE { namespace UI {
class EE_API UIRichText : public UILayout {
class EE_API UIRichText : public UIHTMLWidget {
public:
enum class IntrinsicMode { None, Min, Max };
static void rebuildRichText( UILayout* container, RichText& richText,
IntrinsicMode mode = IntrinsicMode::None );
static UIRichText* New();
static UIRichText* NewWithTag( const std::string& tag );
@@ -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

View File

@@ -2,13 +2,14 @@
#define EE_UI_UITEXTSPAN_HPP
#include <eepp/ui/uifontstyleconfig.hpp>
#include <eepp/ui/uihtmlwidget.hpp>
#include <eepp/ui/uiwidget.hpp>
namespace EE { namespace UI {
using SpanHitBoxes = SmallVector<Rectf, 4>;
class EE_API UITextSpan : public UIWidget {
class EE_API UITextSpan : public UIHTMLWidget {
public:
static UITextSpan* New();
@@ -32,6 +33,8 @@ class EE_API UITextSpan : public UIWidget {
static UITextSpan* NewCode() { return NewWithTag( "code" ); }
static UITextSpan* NewSmall() { return NewWithTag( "small" ); }
virtual ~UITextSpan();
virtual Uint32 getType() const;

View File

@@ -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.

View File

@@ -0,0 +1,88 @@
#include <eepp/ui/csslayouttypes.hpp>
namespace EE { namespace UI {
std::string CSSDisplayHelper::toString( CSSDisplay display ) {
switch ( display ) {
case CSSDisplay::Inline:
return "inline";
case CSSDisplay::InlineBlock:
return "inline-block";
case CSSDisplay::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

View File

@@ -0,0 +1,243 @@
#include <eepp/graphics/richtext.hpp>
#include <eepp/ui/blocklayouter.hpp>
#include <eepp/ui/uihtmlwidget.hpp>
#include <eepp/ui/uirichtext.hpp>
#include <eepp/ui/uistyle.hpp>
#include <eepp/ui/uitextspan.hpp>
namespace EE { namespace UI {
Float BlockLayouter::getMinIntrinsicWidth() {
computeIntrinsicWidths();
return mMinIntrinsicWidth;
}
Float BlockLayouter::getMaxIntrinsicWidth() {
computeIntrinsicWidths();
return mMaxIntrinsicWidth;
}
void BlockLayouter::computeIntrinsicWidths() {
if ( !mContainer->isType( UI_TYPE_HTML_WIDGET ) )
return;
auto* widget = mContainer->asType<UIHTMLWidget>();
auto* rt = widget->getRichTextPtr();
if ( rt == nullptr )
return;
if ( mContainer->getLayoutWidthPolicy() == SizePolicy::Fixed ) {
// Do nothing here, UIWidget handles fixed width.
return;
}
if ( mIntrinsicWidthsDirty ) {
RichText tmpRt( *rt );
UIRichText::rebuildRichText( widget, tmpRt, UIRichText::IntrinsicMode::Min );
mMinIntrinsicWidth = tmpRt.getMinIntrinsicWidth() + mContainer->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<UIHTMLWidget>();
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<RichText::CustomBlock>( 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<Float>::max();
constexpr Float lowF = std::numeric_limits<Float>::lowest();
Rectf bounds( maxF, maxF, lowF, lowF );
Vector2f offset;
Node* p = widget->getParent();
while ( p && p != mContainer ) {
offset += p->isWidget() ? p->asType<UIWidget>()->getPixelsPosition() : p->getPosition();
p = p->getParent();
}
if ( widget->isType( UI_TYPE_TEXTSPAN ) ) {
UITextSpan* textSpan = widget->asType<UITextSpan>();
Int64 startChar = curCharIdx;
Int64 endChar = curCharIdx;
if ( !textSpan->getText().empty() ) {
endChar += textSpan->getText().length();
curCharIdx = endChar;
}
auto& hitBoxes = textSpan->getHitBoxes();
hitBoxes.clear();
if ( startChar < endChar ) {
for ( const auto& line : lines ) {
bool passedText = false;
for ( const auto& rspan : line.spans ) {
if ( rspan.startCharIndex >= startChar && rspan.endCharIndex <= endChar ) {
Rectf hb( mContainer->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<UIWidget>(), 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<UIWidget>()->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<UIWidget>(), processWidget );
}
child = child->getNextNode();
}
}
}} // namespace EE::UI

View File

@@ -425,6 +425,15 @@ void StyleSheetSpecification::registerDefaultProperties() {
registerProperty( "cols", "20" ).setType( PropertyType::NumberInt );
registerProperty( "input-mode", "normal" ).setType( PropertyType::String );
registerProperty( "hidden", "" ).setType( PropertyType::Bool );
registerProperty( "display", "inline" ).setType( PropertyType::String );
registerProperty( "position", "static" ).setType( PropertyType::String );
registerProperty( "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 );

View File

@@ -0,0 +1,533 @@
#include <eepp/ui/tablelayouter.hpp>
#include <eepp/ui/uihtmltable.hpp>
#include <eepp/ui/uistyle.hpp>
namespace EE { namespace UI {
static inline Float sanitizeFloat( Float val ) {
return std::isfinite( val ) ? val : 0.f;
}
void TableLayouter::setTableLayout( TableLayout layout ) {
if ( layout != mTableLayout ) {
mTableLayout = layout;
}
}
TableLayout TableLayouter::getTableLayout() const {
return mTableLayout;
}
void TableLayouter::setCellpadding( Float padding ) {
mCellpadding = padding;
}
Float TableLayouter::getCellpadding() const {
return mCellpadding;
}
void TableLayouter::setCellspacing( Float spacing ) {
mCellspacing = spacing;
}
Float TableLayouter::getCellspacing() const {
return mCellspacing;
}
Float TableLayouter::getMinIntrinsicWidth() {
computeIntrinsicWidths();
return mMinIntrinsicWidth;
}
Float TableLayouter::getMaxIntrinsicWidth() {
computeIntrinsicWidths();
return mMaxIntrinsicWidth;
}
void TableLayouter::computeIntrinsicWidths() {
if ( !mIntrinsicWidthsDirty )
return;
mRows.clear();
mHead = nullptr;
mBody = nullptr;
mFooter = nullptr;
mCells.clear();
mRowCellOffsets.clear();
auto collectRows = [&]( auto&& self, Node* node ) -> void {
for ( Node* child = node->getFirstChild(); child; child = child->getNextNode() ) {
if ( child->getType() == UI_TYPE_HTML_TABLE_ROW ) {
mRows.push_back( child->asType<UIHTMLTableRow>() );
} else if ( child->getType() != UI_TYPE_HTML_TABLE ) {
if ( child->getType() == UI_TYPE_HTML_TABLE_HEAD )
mHead = child->asType<UIHTMLTableHead>();
else if ( child->getType() == UI_TYPE_HTML_TABLE_BODY )
mBody = child->asType<UIHTMLTableBody>();
else if ( child->getType() == UI_TYPE_HTML_TABLE_FOOTER )
mFooter = child->asType<UIHTMLTableFooter>();
self( self, child );
}
}
};
collectRows( collectRows, mContainer );
auto getRecursiveSpecifiedWidth = [&]( auto&& self, Node* node ) -> Float {
if ( !node->isWidget() )
return 0.f;
UIWidget* widget = node->asType<UIWidget>();
Float spec = 0.f;
if ( widget->getLayoutWidthPolicy() == SizePolicy::Fixed )
spec = sanitizeFloat( widget->getPropertyWidth() );
for ( Node* child = node->getFirstChild(); child; child = child->getNextNode() )
spec = std::max( spec, self( self, child ) );
return spec;
};
if ( mRows.empty() ) {
mMinIntrinsicWidth = mMaxIntrinsicWidth =
mContainer->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<UIHTMLTableCell>();
mCells.push_back( cell );
colCount += cell->getColspan();
if ( mCellpadding > 0 && cell->getPadding() == Rectf::Zero ) {
cell->setPadding( { mCellpadding, mCellpadding, mCellpadding, mCellpadding } );
}
}
}
mRowCellOffsets.push_back( static_cast<Uint32>( mCells.size() ) );
maxCols = std::max( maxCols, colCount );
}
if ( maxCols == 0 ) {
mMinIntrinsicWidth = mMaxIntrinsicWidth =
mContainer->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<UIWidget>();
if ( widget->getLayoutWidthPolicy() == SizePolicy::Fixed && widget->getUIStyle() &&
( prop = widget->getUIStyle()->getProperty( PropertyId::Width ) ) ) {
widget->asType<UINode>()->setInternalPixelsSize(
{ widget->lengthFromValue( *prop ), widget->getPixelsSize().getHeight() } );
}
computeIntrinsicWidths();
if ( mRows.empty() ) {
mPacking = false;
return;
}
size_t maxCols = mColMinWidths.size();
mColWidths.assign( maxCols, 0.f );
Float paddingH = mContainer->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<Float>( unspecifiedCount );
for ( size_t i = 0; i < maxCols; ++i ) {
if ( mColSpecifiedWidths[i] <= 0.f ) {
mColWidths[i] = share;
}
}
} else if ( remainingSpace > 0.f && sumOfSpecifiedWidths > 0.f ) {
for ( size_t i = 0; i < maxCols; ++i ) {
Float scale = mColSpecifiedWidths[i] / sumOfSpecifiedWidths;
mColWidths[i] += remainingSpace * scale;
}
}
} else if ( tableUsedWidth <= totalMin + 0.001f ) {
Float scale = totalMin > 0.001f ? ( tableUsedWidth / totalMin ) : 0.f;
for ( size_t i = 0; i < maxCols; ++i )
mColWidths[i] = mColMinWidths[i] * scale;
} else if ( tableUsedWidth <= totalMax + 0.001f ) {
Float extraSpace = tableUsedWidth - totalMin;
Float totalFlex = 0.f;
for ( size_t i = 0; i < maxCols; ++i ) {
Float flex = mColMaxWidths[i] - mColMinWidths[i];
if ( mColSpecifiedWidths[i] > 0.f )
flex = 0.f;
totalFlex += flex;
}
if ( totalFlex > 0.001f ) {
for ( size_t i = 0; i < maxCols; ++i ) {
Float flex = mColMaxWidths[i] - mColMinWidths[i];
if ( mColSpecifiedWidths[i] > 0.f )
flex = 0.f;
Float added = extraSpace * ( flex / totalFlex );
mColWidths[i] = mColMinWidths[i] + added;
}
} else {
Float scale = totalMin > 0.001f ? ( tableUsedWidth / totalMin ) : 0.f;
for ( size_t i = 0; i < maxCols; ++i )
mColWidths[i] = mColMinWidths[i] * scale;
}
} else {
Float leftOver = tableUsedWidth - totalMax;
Float totalMaxUnspecified = 0.f;
size_t unspecifiedCount = 0;
for ( size_t i = 0; i < maxCols; ++i ) {
if ( mColSpecifiedWidths[i] <= 0.f ) {
totalMaxUnspecified += mColMaxWidths[i];
unspecifiedCount++;
}
}
if ( unspecifiedCount > 0 ) {
if ( totalMaxUnspecified > 0.001f ) {
for ( size_t i = 0; i < maxCols; ++i ) {
if ( mColSpecifiedWidths[i] <= 0.f ) {
Float scale = mColMaxWidths[i] / totalMaxUnspecified;
mColWidths[i] = mColMaxWidths[i] + ( leftOver * scale );
} else {
mColWidths[i] = mColMaxWidths[i];
}
}
} else {
Float share = leftOver / static_cast<Float>( unspecifiedCount );
for ( size_t i = 0; i < maxCols; ++i ) {
if ( mColSpecifiedWidths[i] <= 0.f ) {
mColWidths[i] = mColMaxWidths[i] + share;
} else {
mColWidths[i] = mColMaxWidths[i];
}
}
}
} else {
Float scale = totalMax > 0.001f ? ( tableUsedWidth / totalMax ) : 0.f;
for ( size_t i = 0; i < maxCols; ++i )
mColWidths[i] = mColMaxWidths[i] * scale;
}
}
Float sum = 0.f;
for ( float w : mColWidths )
sum += w;
if ( sum < 1.f && maxCols > 0 ) {
Float w = tableUsedWidth / static_cast<Float>( maxCols );
for ( size_t i = 0; i < maxCols; ++i )
mColWidths[i] = w;
}
for ( float& w : mColWidths )
w = sanitizeFloat( w );
Float headHeight = 0;
Float bodyHeight = 0;
Float footerHeight = 0;
size_t rowCount = mRows.size();
for ( size_t r = 0; r < rowCount; ++r ) {
Float rowHeight = 0;
Uint32 start = mRowCellOffsets[r];
Uint32 end = mRowCellOffsets[r + 1];
Uint32 columnCount = end - start;
Uint32 colIndex = 0;
for ( Uint32 c = 0; c < columnCount; ++c ) {
UIHTMLTableCell* cell = mCells[start + c];
cell->beginAttributesTransaction();
cell->setLayoutWidthPolicy( SizePolicy::Fixed );
cell->setLayoutHeightPolicy( SizePolicy::WrapContent );
Uint32 cellColspan = cell->getColspan();
Float cellWidth = 0;
for ( Uint32 j = 0; j < cellColspan && ( colIndex + j ) < maxCols; ++j ) {
cellWidth += mColWidths[colIndex + j];
}
if ( cellColspan > 1 )
cellWidth += ( cellColspan - 1 ) * mCellspacing;
cell->setPixelsSize( cellWidth, cell->getPixelsSize().getHeight() );
cell->updateLayout();
cell->setLayoutHeightPolicy( SizePolicy::Fixed );
cell->endAttributesTransaction();
rowHeight = std::max( rowHeight, cell->getPixelsSize().getHeight() );
colIndex += cellColspan;
}
Float currentX = mCellspacing;
colIndex = 0;
for ( Uint32 c = 0; c < columnCount; ++c ) {
UIHTMLTableCell* cell = mCells[start + c];
cell->beginAttributesTransaction();
cell->setPixelsPosition( currentX, 0 );
Uint32 cellColspan = cell->getColspan();
Float cellWidth = 0;
for ( Uint32 j = 0; j < cellColspan && ( colIndex + j ) < maxCols; ++j ) {
cellWidth += mColWidths[colIndex + j];
}
if ( cellColspan > 1 )
cellWidth += ( cellColspan - 1 ) * mCellspacing;
cell->setPixelsSize( cellWidth, rowHeight );
cell->endAttributesTransaction();
currentX += cellWidth + mCellspacing;
colIndex += cellColspan;
}
UIHTMLTableRow* row = mRows[r];
row->setPixelsSize( containerWidth - paddingH, rowHeight );
if ( r == 0 && mCells[start]->getParent()->isType( UI_TYPE_HTML_TABLE_HEAD ) ) {
headHeight = rowHeight;
} else if ( r == rowCount - 1 && columnCount &&
mCells[start]->getParent()->isType( UI_TYPE_HTML_TABLE_FOOTER ) ) {
footerHeight = rowHeight;
} else {
bodyHeight += rowHeight;
}
}
if ( mHead ) {
mHead->setPixelsPosition( 0, 0 );
mHead->setPixelsSize( { mContainer->getPixelsSize().x, headHeight } );
}
if ( mBody ) {
mBody->setPixelsPosition( 0, headHeight );
mBody->setPixelsSize( { mContainer->getPixelsSize().x, bodyHeight } );
}
if ( mFooter ) {
mFooter->setPixelsPosition( 0, headHeight + bodyHeight );
mFooter->setPixelsSize( { mContainer->getPixelsSize().x, footerHeight } );
}
Float currentY = mContainer->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<UINode>()->setInternalPixelsHeight(
mContainer->getPixelsPadding().Top + headHeight + bodyHeight + footerHeight +
( rowCount + 1 ) * mCellspacing + mContainer->getPixelsPadding().Bottom );
}
mPacking = false;
}
}} // namespace EE::UI

View File

@@ -1,42 +1,27 @@
#include "eepp/ui/uistyle.hpp"
#include <algorithm>
#include <cmath>
#include <eepp/ui/tablelayouter.hpp>
#include <eepp/ui/uihtmltable.hpp>
#include <eepp/ui/uilayouter.hpp>
#include <eepp/ui/uistyle.hpp>
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<UIHTMLTable*>( this )->getLayouter() ) {
static_cast<TableLayouter*>( const_cast<UIHTMLTable*>( this )->getLayouter() )
->setCellspacing( lengthFromValue( attribute ) );
invalidateIntrinsicSize();
tryUpdateLayout();
}
return true;
case PropertyId::Cellpadding:
mCellpadding = lengthFromValue( attribute );
invalidateIntrinsicSize();
tryUpdateLayout();
if ( const_cast<UIHTMLTable*>( this )->getLayouter() ) {
static_cast<TableLayouter*>( const_cast<UIHTMLTable*>( 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<UIHTMLTable*>( this )->getLayouter() ) {
static_cast<TableLayouter*>( const_cast<UIHTMLTable*>( 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<UIHTMLTable*>( 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<UIHTMLTableRow>() );
} else if ( child->getType() != UI_TYPE_HTML_TABLE ) {
if ( child->getType() == UI_TYPE_HTML_TABLE_HEAD )
me->mHead = child->asType<UIHTMLTableHead>();
else if ( child->getType() == UI_TYPE_HTML_TABLE_BODY )
me->mBody = child->asType<UIHTMLTableBody>();
else if ( child->getType() == UI_TYPE_HTML_TABLE_FOOTER )
me->mFooter = child->asType<UIHTMLTableFooter>();
self( self, child );
}
}
};
collectRows( collectRows, me );
auto getRecursiveSpecifiedWidth = [&]( auto&& self, Node* node ) -> Float {
if ( !node->isWidget() )
return 0.f;
UIWidget* widget = node->asType<UIWidget>();
Float spec = 0.f;
if ( widget->getLayoutWidthPolicy() == SizePolicy::Fixed )
spec = sanitizeFloat( widget->getPropertyWidth() );
for ( Node* child = node->getFirstChild(); child; child = child->getNextNode() )
spec = std::max( spec, self( self, child ) );
return spec;
};
if ( mRows.empty() ) {
mMinIntrinsicWidth = mMaxIntrinsicWidth = 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<UIHTMLTableCell>();
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<Uint32>( 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<UIHTMLTable*>( this )->getLayouter();
if ( layouter )
layouter->computeIntrinsicWidths();
}
Float UIHTMLTable::getMinIntrinsicWidth() const {
computeIntrinsicWidths();
return mMinIntrinsicWidth;
UILayouter* layouter = const_cast<UIHTMLTable*>( this )->getLayouter();
if ( layouter )
return static_cast<TableLayouter*>( layouter )->getMinIntrinsicWidth();
return 0;
}
Float UIHTMLTable::getMaxIntrinsicWidth() const {
computeIntrinsicWidths();
return mMaxIntrinsicWidth;
UILayouter* layouter = const_cast<UIHTMLTable*>( this )->getLayouter();
if ( layouter )
return static_cast<TableLayouter*>( layouter )->getMaxIntrinsicWidth();
return 0;
}
void UIHTMLTable::updateLayout() {
if ( mPacking || !mVisible )
return;
mPacking = true;
setMatchParentIfNeededVerticalGrowth();
UILayouter* layouter = const_cast<UIHTMLTable*>( 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<Float>( unspecifiedCount );
for ( size_t i = 0; i < maxCols; ++i ) {
if ( mColSpecifiedWidths[i] <= 0.f ) {
mColWidths[i] = share;
}
}
} else if ( remainingSpace > 0.f && sumOfSpecifiedWidths > 0.f ) {
for ( size_t i = 0; i < maxCols; ++i ) {
Float scale = mColSpecifiedWidths[i] / sumOfSpecifiedWidths;
mColWidths[i] += remainingSpace * scale;
}
}
} else if ( tableUsedWidth <= totalMin + 0.001f ) {
// 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<Float>( unspecifiedCount );
for ( size_t i = 0; i < maxCols; ++i ) {
if ( mColSpecifiedWidths[i] <= 0.f ) {
mColWidths[i] = mColMaxWidths[i] + share;
} else {
mColWidths[i] = mColMaxWidths[i];
}
}
}
} else {
// 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<Float>( maxCols );
for ( size_t i = 0; i < maxCols; ++i )
mColWidths[i] = w;
}
for ( float& w : mColWidths )
w = sanitizeFloat( w );
Float headHeight = 0;
Float bodyHeight = 0;
Float footerHeight = 0;
// 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

View File

@@ -0,0 +1,158 @@
#include <eepp/ui/css/propertydefinition.hpp>
#include <eepp/ui/uihtmlwidget.hpp>
#include <eepp/ui/uilayouter.hpp>
#include <eepp/ui/uilayoutermanager.hpp>
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<UIHTMLWidget*>( 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

View File

@@ -0,0 +1,10 @@
#include <eepp/ui/uilayout.hpp>
#include <eepp/ui/uilayouter.hpp>
namespace EE { namespace UI {
void UILayouter::setMatchParentIfNeededVerticalGrowth() {
mContainer->asType<UILayout>()->setMatchParentIfNeededVerticalGrowth();
}
}} // namespace EE::UI

View File

@@ -0,0 +1,28 @@
#include <eepp/core/memorymanager.hpp>
#include <eepp/ui/blocklayouter.hpp>
#include <eepp/ui/inlinelayouter.hpp>
#include <eepp/ui/nonelayouter.hpp>
#include <eepp/ui/tablelayouter.hpp>
#include <eepp/ui/uilayouter.hpp>
#include <eepp/ui/uilayoutermanager.hpp>
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

View File

@@ -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(),

View File

@@ -6,6 +6,9 @@
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uithememanager.hpp>
#define PUGIXML_HEADER_ONLY
#include <pugixml/pugixml.hpp>
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

View File

@@ -3,6 +3,7 @@
#include <eepp/ui/css/propertydefinition.hpp>
#include <eepp/ui/tools/htmlformatter.hpp>
#include <eepp/ui/uicodeeditor.hpp>
#include <eepp/ui/uilayouter.hpp>
#include <eepp/ui/uirichtext.hpp>
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uistyle.hpp>
@@ -15,24 +16,13 @@
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 ) );
}
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;
@@ -42,6 +32,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 +114,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 +141,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 +546,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 +593,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 +619,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 +633,7 @@ void UIRichText::rebuildRichText( RichText& richText, IntrinsicMode mode ) {
}
};
Node* child = mChild;
Node* child = container->getFirstChild();
while ( NULL != child ) {
if ( child->isWidget() ) {
processWidget( child->asType<UIWidget>(), processWidget );
@@ -633,140 +642,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<RichText::CustomBlock>( 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<Float>::max();
constexpr Float lowF = std::numeric_limits<Float>::lowest();
Rectf bounds( maxF, maxF, lowF, lowF );
Vector2f offset;
Node* p = widget->getParent();
while ( p && p != this ) {
offset += p->isWidget() ? p->asType<UIWidget>()->getPixelsPosition() : p->getPosition();
p = p->getParent();
}
if ( widget->isType( UI_TYPE_TEXTSPAN ) ) {
UITextSpan* textSpan = widget->asType<UITextSpan>();
Int64 startChar = curCharIdx;
Int64 endChar = curCharIdx;
if ( !textSpan->getText().empty() ) {
endChar += textSpan->getText().length();
curCharIdx = endChar;
}
auto& hitBoxes = textSpan->getHitBoxes();
hitBoxes.clear();
if ( startChar < endChar ) {
for ( const auto& line : lines ) {
bool passedText = false;
for ( const auto& rspan : line.spans ) {
if ( rspan.startCharIndex >= startChar && rspan.endCharIndex <= endChar ) {
Rectf hb( 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<UIWidget>(), 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<UIWidget>()->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<UIWidget>(), processWidget );
}
child = child->getNextNode();
}
void UIRichText::rebuildRichText( RichText& richText, IntrinsicMode mode ) {
rebuildRichText( this, richText, mode );
}
void UIRichText::updateDefaultSpansStyle() {
@@ -780,51 +657,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 +671,18 @@ Float UIRichText::getMinIntrinsicWidth() const {
return getPropertyWidth();
}
if ( mIntrinsicWidthsDirty ) {
UILayouter* layouter = const_cast<UIRichText*>( this )->getLayouter();
if ( mIntrinsicWidthsDirty && layouter ) {
layouter->computeIntrinsicWidths();
mMinIntrinsicWidth = layouter->getMinIntrinsicWidth();
mMaxIntrinsicWidth = layouter->getMaxIntrinsicWidth();
} else if ( mIntrinsicWidthsDirty ) {
RichText richText( mRichText );
const_cast<UIRichText*>( this )->rebuildRichText( richText, IntrinsicMode::Min );
UIRichText::rebuildRichText( const_cast<UIRichText*>( this ), richText,
IntrinsicMode::Min );
mMinIntrinsicWidth = richText.getMinIntrinsicWidth() + mPaddingPx.Left + mPaddingPx.Right;
const_cast<UIRichText*>( this )->rebuildRichText( richText, IntrinsicMode::Max );
UIRichText::rebuildRichText( const_cast<UIRichText*>( this ), richText,
IntrinsicMode::Max );
mMaxIntrinsicWidth = richText.getMaxIntrinsicWidth() + mPaddingPx.Left + mPaddingPx.Right;
mIntrinsicWidthsDirty = false;
}
@@ -854,16 +700,24 @@ Float UIRichText::getMaxIntrinsicWidth() const {
return getPropertyWidth();
}
if ( mIntrinsicWidthsDirty ) {
RichText richText( mRichText );
const_cast<UIRichText*>( this )->rebuildRichText( richText, IntrinsicMode::Min );
mMinIntrinsicWidth = richText.getMinIntrinsicWidth() + mPaddingPx.Left + mPaddingPx.Right;
const_cast<UIRichText*>( this )->rebuildRichText( richText, IntrinsicMode::Max );
mMaxIntrinsicWidth = richText.getMaxIntrinsicWidth() + mPaddingPx.Left + mPaddingPx.Right;
mIntrinsicWidthsDirty = false;
Float maxW = 0;
if ( const_cast<UIRichText*>( this )->getLayouter() ) {
maxW = const_cast<UIRichText*>( this )->getLayouter()->getMaxIntrinsicWidth();
} else {
if ( mIntrinsicWidthsDirty ) {
RichText richText( mRichText );
const_cast<UIRichText*>( this )->rebuildRichText( richText, IntrinsicMode::Min );
mMinIntrinsicWidth =
richText.getMinIntrinsicWidth() + mPaddingPx.Left + mPaddingPx.Right;
const_cast<UIRichText*>( 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 +729,7 @@ Uint32 UIRichText::onMessage( const NodeMessage* Msg ) {
switch ( Msg->getMsg() ) {
case NodeMessage::LayoutAttributeChange: {
if ( Msg->getSender() != this && !mPacking ) {
mIntrinsicWidthsDirty = true;
invalidateIntrinsicSize();
notifyLayoutAttrChangeParent();
}
tryUpdateLayout();

View File

@@ -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();
}

View File

@@ -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();

View File

@@ -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:

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -156,14 +156,14 @@ static const auto LAYOUT = R"xml(
</vbox>
</vbox>
<vbox class="right" lw="0" lh="wc" lw8="0.5" lg="center">
<button id="create-new" text="@string(new_file, New File)" />
<button id="create-new-terminal" text="@string(new_terminal, New Terminal)" />
<button id="open-folder" text="@string(open_a_folder, Open a Folder)" />
<button id="open-file" text="@string(open_a_file, Open a File)" />
<button id="recent-folders" text="@string(recent_folders_ellipsis, Recent Folders...)" />
<button id="recent-files" text="@string(recent_files_ellipsis, Recent Files...)" />
<button id="plugin-manager-open" text="@string(plugin_manager, Plugins Manager)" />
<button id="keybindings" text="@string(keybindings, Keybindings)" />
<PushButton id="create-new" text="@string(new_file, New File)" />
<PushButton id="create-new-terminal" text="@string(new_terminal, New Terminal)" />
<PushButton id="open-folder" text="@string(open_a_folder, Open a Folder)" />
<PushButton id="open-file" text="@string(open_a_file, Open a File)" />
<PushButton id="recent-folders" text="@string(recent_folders_ellipsis, Recent Folders...)" />
<PushButton id="recent-files" text="@string(recent_files_ellipsis, Recent Files...)" />
<PushButton id="plugin-manager-open" text="@string(plugin_manager, Plugins Manager)" />
<PushButton id="keybindings" text="@string(keybindings, Keybindings)" />
<widget class="separator" lw="mp" lh="32dp" />
<tv class="bold" text="@string(for_help_please_visit, For help, please visit:)" lg="center" />
<vbox lw="wc" lh="wc" lg="center">