More fixes.

This commit is contained in:
Martín Lucas Golini
2026-05-24 13:43:29 -03:00
parent aa9efcf13b
commit c5e39f4c6d
8 changed files with 101 additions and 14 deletions

View File

@@ -16,7 +16,7 @@ The target is not a Reddit-specific hack. Each fix must move the HTML/CSS engine
- Reference image: `bin/unit_tests/assets/html/reddit_old_thread_reference_image.png`
- Smoke test: `UIHTML.redditOldThreadWebViewSmoke`
The smoke test loads `assets/ui/breeze.css` first and then opens the old Reddit fixture through `UIWebView`, so it follows the same path as the browser-like local workflow. It asserts the important page regions exist and emits `assets/html/eepp-reddit-old-thread-current.webp` through the existing image comparison helper.
The smoke test opens the old Reddit fixture through `UIWebView` without loading the app theme. Browser-like HTML defaults are supplied by the HTML base defaults stylesheet when HTML widgets enter the node tree, so the test exercises the same default-style path used by real HTML content. It asserts the important page regions exist and emits `assets/html/eepp-reddit-old-thread-current.webp` through the existing image comparison helper.
The smoke test is opt-in because the full fixture is slow in ASAN:
@@ -38,7 +38,10 @@ Current progress:
- `BlockLayouter` now forwards inherited float exclusions through normal non-floating blocks and preserves the parent-computed used width for BFC match-parent children next to floats.
- The fixture now keeps the main `.entry` selftext box to the left of `.side` (`side.x=719`, `entry.x=44`, `entry.width=670` in the current smoke run).
- External float exclusions are filtered for fixed-width descendants whose content box is entirely outside the float's horizontal range. This keeps the comment form textarea from being pushed below the sidebar while still letting match-parent content compute a float-constrained used width.
- Remaining visible blockers: header/topbar layout is still badly overlapped, the vote arrow sprites/centering are incomplete, the comment form spacing is too large, and the footer/comments vertical spacing still diverges from Chrome.
- The smoke test no longer loads `breeze.css`; HTML defaults now come from automatic HTML base-default injection.
- Legacy `<strike>` is registered as an HTML phrasing element and receives default `line-through` styling, removing a missing-element warning from the fixture.
- Horizontal `auto` margins are recomputed at RichText/block layout read points, and the old Reddit vote arrow is centered inside `.midcol` (`arrow.x=17`, `midcol.x=15`, `arrow.width=15`, `midcol.width=19` in the current smoke run).
- Remaining visible blockers: header/topbar layout is still badly overlapped, vote arrow sprite painting/background positioning still needs verification, the comment form spacing is too large, and the footer/comments vertical spacing still diverges from Chrome.
## Reference Layout Invariants
@@ -133,7 +136,7 @@ Reduced tests:
### 4. CSS Display Defaults And Element Creation
The user currently loads `breeze.css` because not all base HTML element display/state defaults have moved into HTML element creation. This should be reduced.
The old `breeze.css` dependency has been removed from the old Reddit smoke test. HTML base defaults are now injected automatically when HTML content is attached to a scene.
Old Reddit relies on browser defaults for:
@@ -142,7 +145,7 @@ Old Reddit relies on browser defaults for:
- replaced/form controls such as `input`, `textarea`
- hidden inputs and `display:none` nodes
Plan:
Remaining plan:
- Audit `UIWidgetCreator` and HTML element constructors for browser-like default display.
- Keep theme-specific visual styling in CSS, but move semantic defaults into element creation.
@@ -329,12 +332,12 @@ Exit criteria:
### Phase 4: Defaults And Form Controls
Move semantic HTML defaults out of theme dependency where practical, keeping `breeze.css` as a visual theme rather than a behavior crutch.
Keep semantic HTML defaults independent from the app theme. `breeze.css` should remain a visual UI theme, not a behavior crutch for HTML content.
Exit criteria:
- Minimal HTML default-display tests pass without loading `breeze.css`.
- The old Reddit smoke test still passes with breeze loaded.
- The old Reddit smoke test passes without loading `breeze.css`.
- Form controls in the comment box and sidebar match expected broad geometry.
### Phase 5: Full-Page Visual Gate

View File

@@ -328,6 +328,10 @@ class EE_API UIWidget : public UINode {
bool hasLayoutMarginBottomAuto() const;
bool hasLayoutMarginAuto() const;
UIWidget* updateLayoutMarginAuto();
/**
* @brief Sets the layout margin for all sides in pixels.
*

View File

@@ -535,6 +535,8 @@ void BlockLayouter::positionRichTextChildren( Graphics::RichText* rt ) {
curCharIdx += 1;
Rectf atomicBounds( maxF, maxF, lowF, lowF );
if ( getAtomicWidgetFragmentBounds( widget, atomicBounds ) ) {
if ( widget->hasLayoutMarginAuto() )
widget->updateLayoutMarginAuto();
Rectf margin =
widget->isType( UI_TYPE_HTML_WIDGET )
? widget->asType<UIHTMLWidget>()->getNormalFlowLayoutPixelsMargin()
@@ -574,6 +576,8 @@ void BlockLayouter::positionRichTextChildren( Graphics::RichText* rt ) {
size_t lineIdx = currentSpan > 0 ? currentLine : currentLine - 1;
Float lineY = lines[lineIdx].y;
if ( widget->hasLayoutMarginAuto() )
widget->updateLayoutMarginAuto();
Rectf margin =
widget->isType( UI_TYPE_HTML_WIDGET )
? widget->asType<UIHTMLWidget>()->getNormalFlowLayoutPixelsMargin()

View File

@@ -1307,6 +1307,8 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
"\n", widget->asType<UILineBreak>()->getRichText().getFontStyleConfig() );
lastSpanEndsWithSpace = false;
} else {
if ( widget->hasLayoutMarginAuto() )
widget->updateLayoutMarginAuto();
Rectf margin =
widget->isType( UI_TYPE_HTML_WIDGET )
? widget->asType<UIHTMLWidget>()->getNormalFlowLayoutPixelsMargin()

View File

@@ -188,7 +188,7 @@ UIWidget* UIWidget::setLayoutMarginTopAuto( bool isAuto ) {
}
UIWidget* UIWidget::setLayoutMarginBottomAuto( bool isAuto ) {
return setLayoutMarginAuto( MarginAuto::Top, isAuto );
return setLayoutMarginAuto( MarginAuto::Bottom, isAuto );
}
UIWidget* UIWidget::setLayoutMarginAuto( bool left, bool right, bool top, bool bottom ) {
@@ -215,6 +215,15 @@ bool UIWidget::hasLayoutMarginBottomAuto() const {
return mMarginAuto & MarginAuto::Bottom;
}
bool UIWidget::hasLayoutMarginAuto() const {
return mMarginAuto != 0;
}
UIWidget* UIWidget::updateLayoutMarginAuto() {
calculateAutoMargin();
return this;
}
UIWidget* UIWidget::setLayoutPixelsMargin( const Rectf& margin ) {
if ( mLayoutMargin != margin ) {
mLayoutMarginPx = margin;

View File

@@ -88,7 +88,7 @@ b, strong { font-weight: bold; }
i, em, cite { font-style: italic; }
small { font-size: smaller; }
u, ins { text-decoration: underline; }
s, del { text-decoration: line-through; }
s, strike, del { text-decoration: line-through; }
code, kbd { font-family: monospace; }
sub, sup { font-size: smaller; }
mark { background-color: yellow; }
@@ -182,7 +182,6 @@ RadioButton::active {
}
)css";
}
void UIWidgetCreator::createBaseWidgetList() {
@@ -273,6 +272,7 @@ void UIWidgetCreator::createBaseWidgetList() {
registeredWidget["u"] = UITextSpan::NewUnderline;
registeredWidget["ins"] = UITextSpan::NewUnderline;
registeredWidget["s"] = UITextSpan::NewStrikethrough;
registeredWidget["strike"] = [] { return UITextSpan::NewWithTag( "strike" ); };
registeredWidget["del"] = UITextSpan::NewStrikethrough;
registeredWidget["font"] = UITextSpan::NewFont;
registeredWidget["code"] = UITextSpan::NewCode;

View File

@@ -16,6 +16,7 @@
using namespace EE;
using namespace EE::UI;
using namespace EE::UI::Tools;
using namespace EE::Window;
using namespace EE::Graphics;
@@ -276,6 +277,34 @@ UTEST( UIHTMLFloat, leftFloatOverflowHiddenBlockFormattingContextSitsBesideFloat
Engine::destroySingleton();
}
UTEST( UIHTMLFloat, autoHorizontalMarginsCenterBlockInsideFloat ) {
init_float_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
<body style="margin:0">
<div id="midcol" style="float:left;width:19px;height:50px;overflow:hidden">
<div id="arrow" style="display:block;width:15px;height:14px;margin-left:auto;margin-right:auto"></div>
</div>
</body>
)html" ) );
sceneNode->updateDirtyLayouts();
auto* midcol = sceneNode->find<UIWidget>( "midcol" );
auto* arrow = sceneNode->find<UIWidget>( "arrow" );
ASSERT_TRUE( midcol != nullptr );
ASSERT_TRUE( arrow != nullptr );
Vector2f midcolPos = midcol->convertToWorldSpace( { 0, 0 } );
Vector2f arrowPos = arrow->convertToWorldSpace( { 0, 0 } );
Float midcolCenter = midcolPos.x + midcol->getPixelsSize().getWidth() / 2.f;
Float arrowCenter = arrowPos.x + arrow->getPixelsSize().getWidth() / 2.f;
EXPECT_NEAR( midcolCenter, arrowCenter, 1.f );
Engine::destroySingleton();
}
UTEST( UIHTMLFloat, rightFloatConstrainsTextInsideFollowingNormalBlock ) {
init_float_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();

View File

@@ -189,10 +189,7 @@ UTEST( UIHTML, redditOldThreadWebViewSmoke ) {
UI::UISceneNode* sceneNode = UI::UISceneNode::New();
SceneManager::instance()->add( sceneNode );
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
UITheme* theme = UITheme::load( "breeze", "breeze", "", font, "assets/ui/breeze.css" );
ASSERT_TRUE( theme != nullptr );
sceneNode->setStyleSheet( theme->getStyleSheet() );
themeManager->setDefaultFont( font )->setDefaultTheme( theme )->add( theme );
themeManager->setDefaultFont( font );
UIWebView* webView = UIWebView::New();
webView->setParent( sceneNode->getRoot() );
@@ -230,6 +227,7 @@ UTEST( UIHTML, redditOldThreadWebViewSmoke ) {
Vector2f contentPos = content->convertToWorldSpace( { 0, 0 } );
Vector2f midcolPos = midcol->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f entryPos = entry->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f arrowPos = arrow->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
std::cerr << "old reddit rects: "
<< "side=(" << sidePos.x << "," << sidePos.y << " "
@@ -243,7 +241,16 @@ UTEST( UIHTML, redditOldThreadWebViewSmoke ) {
<< midcol->asType<UIWidget>()->getPixelsSize().getHeight() << ") "
<< "entry=(" << entryPos.x << "," << entryPos.y << " "
<< entry->asType<UIWidget>()->getPixelsSize().getWidth() << "x"
<< entry->asType<UIWidget>()->getPixelsSize().getHeight() << ")" << std::endl;
<< entry->asType<UIWidget>()->getPixelsSize().getHeight() << ") "
<< "arrow=(" << arrowPos.x << "," << arrowPos.y << " "
<< arrow->asType<UIWidget>()->getPixelsSize().getWidth() << "x"
<< arrow->asType<UIWidget>()->getPixelsSize().getHeight() << ")" << std::endl;
const Float midcolCenter =
midcolPos.x + midcol->asType<UIWidget>()->getPixelsSize().getWidth() / 2.f;
const Float arrowCenter =
arrowPos.x + arrow->asType<UIWidget>()->getPixelsSize().getWidth() / 2.f;
EXPECT_NEAR( midcolCenter, arrowCenter, 1.f );
if ( !FileSystem::fileExists( "output" ) )
FileSystem::makeDir( "output" );
@@ -253,6 +260,21 @@ UTEST( UIHTML, redditOldThreadWebViewSmoke ) {
Engine::destroySingleton();
}
UTEST( UIHTML, StrikeElementUsesDefaultLineThrough ) {
init_ui_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML(
R"html(<body><p><strike id="strike">old</strike></p></body>)html" ) );
SceneManager::instance()->update();
auto* strike = sceneNode->getRoot()->find( "strike" )->asType<UITextSpan>();
ASSERT_TRUE( strike != nullptr );
EXPECT_TRUE( 0 != ( strike->getTextDecoration() & Text::StrikeThrough ) );
Engine::destroySingleton();
}
UTEST( UIRichText, anchorMargins ) {
auto win = Engine::instance()->createWindow(
WindowSettings( 800, 600, "Anchor Margins Test", WindowStyle::Default,
@@ -1301,6 +1323,20 @@ UTEST( UILayout, marginAuto ) {
EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Left, expectedMarginX, 1.f );
EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Right, expectedMarginX, 1.f );
childWidget->setLayoutMarginAuto( false, false, true, true );
sceneNode->updateDirtyLayouts();
Float expectedMarginY =
( contWidget->getPixelsSize().getHeight() - childWidget->getPixelsSize().getHeight() ) /
2.f;
EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Top, expectedMarginY, 1.f );
EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Bottom, expectedMarginY, 1.f );
EXPECT_FALSE( childWidget->hasLayoutMarginLeftAuto() );
EXPECT_FALSE( childWidget->hasLayoutMarginRightAuto() );
EXPECT_TRUE( childWidget->hasLayoutMarginTopAuto() );
EXPECT_TRUE( childWidget->hasLayoutMarginBottomAuto() );
Engine::destroySingleton();
}