diff --git a/.agent/plans/reddit_old_thread_rendering_plan.md b/.agent/plans/reddit_old_thread_rendering_plan.md index f003e6356..425de1c1e 100644 --- a/.agent/plans/reddit_old_thread_rendering_plan.md +++ b/.agent/plans/reddit_old_thread_rendering_plan.md @@ -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 `` 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 diff --git a/include/eepp/ui/uiwidget.hpp b/include/eepp/ui/uiwidget.hpp index d05fb17a8..756465641 100644 --- a/include/eepp/ui/uiwidget.hpp +++ b/include/eepp/ui/uiwidget.hpp @@ -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. * diff --git a/src/eepp/ui/blocklayouter.cpp b/src/eepp/ui/blocklayouter.cpp index 0c481d5ea..3b0278ef9 100644 --- a/src/eepp/ui/blocklayouter.cpp +++ b/src/eepp/ui/blocklayouter.cpp @@ -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()->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()->getNormalFlowLayoutPixelsMargin() diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index 374919a8b..9948bcd44 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -1307,6 +1307,8 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri "\n", widget->asType()->getRichText().getFontStyleConfig() ); lastSpanEndsWithSpace = false; } else { + if ( widget->hasLayoutMarginAuto() ) + widget->updateLayoutMarginAuto(); Rectf margin = widget->isType( UI_TYPE_HTML_WIDGET ) ? widget->asType()->getNormalFlowLayoutPixelsMargin() diff --git a/src/eepp/ui/uiwidget.cpp b/src/eepp/ui/uiwidget.cpp index 761b69fc5..84ef53511 100644 --- a/src/eepp/ui/uiwidget.cpp +++ b/src/eepp/ui/uiwidget.cpp @@ -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; diff --git a/src/eepp/ui/uiwidgetcreator.cpp b/src/eepp/ui/uiwidgetcreator.cpp index 74c907270..37bf612e5 100644 --- a/src/eepp/ui/uiwidgetcreator.cpp +++ b/src/eepp/ui/uiwidgetcreator.cpp @@ -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; diff --git a/src/tests/unit_tests/uihtml_float_tests.cpp b/src/tests/unit_tests/uihtml_float_tests.cpp index 58f15b22a..d8409a54a 100644 --- a/src/tests/unit_tests/uihtml_float_tests.cpp +++ b/src/tests/unit_tests/uihtml_float_tests.cpp @@ -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( + +
+
+
+ + )html" ) ); + sceneNode->updateDirtyLayouts(); + + auto* midcol = sceneNode->find( "midcol" ); + auto* arrow = sceneNode->find( "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(); diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp index 24e35bc0e..3622a79e2 100644 --- a/src/tests/unit_tests/uihtml_tests.cpp +++ b/src/tests/unit_tests/uihtml_tests.cpp @@ -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()->convertToWorldSpace( { 0, 0 } ); Vector2f entryPos = entry->asType()->convertToWorldSpace( { 0, 0 } ); + Vector2f arrowPos = arrow->asType()->convertToWorldSpace( { 0, 0 } ); std::cerr << "old reddit rects: " << "side=(" << sidePos.x << "," << sidePos.y << " " @@ -243,7 +241,16 @@ UTEST( UIHTML, redditOldThreadWebViewSmoke ) { << midcol->asType()->getPixelsSize().getHeight() << ") " << "entry=(" << entryPos.x << "," << entryPos.y << " " << entry->asType()->getPixelsSize().getWidth() << "x" - << entry->asType()->getPixelsSize().getHeight() << ")" << std::endl; + << entry->asType()->getPixelsSize().getHeight() << ") " + << "arrow=(" << arrowPos.x << "," << arrowPos.y << " " + << arrow->asType()->getPixelsSize().getWidth() << "x" + << arrow->asType()->getPixelsSize().getHeight() << ")" << std::endl; + + const Float midcolCenter = + midcolPos.x + midcol->asType()->getPixelsSize().getWidth() / 2.f; + const Float arrowCenter = + arrowPos.x + arrow->asType()->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(

old

)html" ) ); + SceneManager::instance()->update(); + + auto* strike = sceneNode->getRoot()->find( "strike" )->asType(); + 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(); }