diff --git a/bin/unit_tests/assets/html/inline_block_wrap_defer.html b/bin/unit_tests/assets/html/inline_block_wrap_defer.html index ec77f64d8..2262c7dc4 100644 --- a/bin/unit_tests/assets/html/inline_block_wrap_defer.html +++ b/bin/unit_tests/assets/html/inline_block_wrap_defer.html @@ -1,220 +1,28 @@ - - - - - - - Space Cadet Pinball on Linux • Stephen Brennan - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - -
- -
- -
- -

Space Cadet Pinball on Linux

-Stephen Brennan • 09 May 2026 - -

To my fellow Linux users who grew up using Windows XP: did you know, you can -have Space Cadet Pinball on your Linux machine? This is not breaking news, but -it’s exciting to me, and I’m the one who decides what I write about. So here’s -your PSA!

- -

Space Cadet Pinball was bundled with Windows XP, and growing up I played it a -lot. As a result it holds a special place in my heart. I found that it was the -most engaging game that was bundled with Windows. Solitaire was too mindless, -and Freecell, Hearts, and Minesweeper were too complex and boring to me at that -age. But pinball held my attention, and so I played it a lot1.

- -

Anyway, somebody has gone to the effort of using a decompiler and reverse -engineering tools to create source code, and then put in what I’d imagine is a -lot of effort to make it playable on a lot of platforms! All that results in -this project on Github. The -easiest way to play on Linux is actually to use the Flatpak, which comes bundled -with the original game resources from the Windows version. You can either -install it with a GUI (e.g. KDE Discover) or install on the CLI:

- -
flatpak install com.github.k4zmu2a.spacecadetpinball
-
- -

That’s all you need to play & get a hit of nostalgia! I know there are -browser-based versions available too, but I would -rather have it installed on my computer directly.

- -

High(er) Resolution With Full Tilt Data

- -

Graphics technology has come a long way since then, and the graphics are a bit -rough at 480p. But fear not: another version of the game existed, called Full -Tilt! Pinball. Its game data is capable of displaying at the massive screen -resolution 1024x768! You can find this game data on -archive.org in a zip file.

- -

Getting the flatpak version to use these data files is a bit tricky. The easiest -way is:

- -
    -
  1. Ensure that you’ve run the game at least once prior to this, so that the data -directory is created.
  2. -
  3. Extract the downloaded zip file directly into your data directory: -
    cd ~/.var/app/com.github.k4zmu2a.spacecadetpinball/data/SpaceCadetPinball
    -unzip ~/Downloads/CADET.ZIP
    -
    -
  4. -
  5. Delete (or, if you’re cautious, rename) the old data directory which is -bundled with the app. Unfortunately this is necessary because the game -searches multiple locations for data, but once it finds data files in one -directory, it won’t continue looking for files in other locations. -
    sudo rm -r $(flatpak info --show-location com.github.k4zmu2a.spacecadetpinball)/files/extra/Pinball
    -
    -

    You may not need the sudo call if your installation was per-user. Mine got -installed to /var/lib/flatpak so I needed root.

    -
  6. -
- -

It’s possible you’d need to repeat step 3 if the game gets updated. However, the -flatpak -hasn’t seen an update in over two years. I doubt one will happen, and if it -does, it won’t be frequent.

- -

Random Notes

- -
    -
  1. -

    If you want, you can keep the original files and merge them together so that -you have the full set of both the original (referred to as “3DPB” for 3D -Pinball in the game) and the new (Full Tilt) data. Then, the game will let -you toggle between them if you’d like.

    -
  2. -
  3. -

    The data files seem to have some impact on the game rules. For instance, in -the original 3DPB version, the reentry lanes (and launch lanes) have lights -which toggle as the ball passes over. In the Full Tilt version, the lights -stay on (rather than toggling), making it easier to complete the set of -lights and upgrade the associated set of bumpers. (Yes yes, I know I’m a nerd -for noticing this.)

    -
  4. -
  5. -

    Apparently, there was a game called “Marble Blast” which came pre-installed -on some Macs in a roughly similar time period, which I know people developed -similar relationships to growing up. Unlike this pinball game, the Marble -Blast series grew, and there are newer versions available to play today.

    -
  6. -
- -

Bonus Thoughts

- -

I think it’s great that this old game was beloved to enough people (and -especially at least one very competent & motivated person) to do this work. -Having (any) source code available makes this game portable to all sorts of -platforms, which is really great. You can play this on Mac, Windows, Linux, and -even Android & Nintendo Switch, apparently.

- -

Personally, I would be happy to pay the original developers for their work on -this game, and I understand that there’s some concern about the legality of -downloading game data files, especially for the Full Tilt version of the game. -After all, they are copyrighted art & data which was part of a commercial -product. It seems like an unpopular opinion in today’s world, but I don’t -advocate for piracy. Paying people for their work is important, even when it -feels like you’re paying a faceless corporation. While I’d prefer things be -created with a FOSS license, the world doesn’t always work that way. At the end -of the day, I want people to be paid to create good things, because that’s how -we get more good things!

- -

On the other hand, I feel software preservation is an important goal too. -Ideally, I’d like to see a world where proprietary software like this could be -placed into some sort of source code -escrow. As long as the -original copyright holders are in the business of selling their product, their -rights should be respected. But if they elect to stop selling it, I think that -code should revert to a FOSS license that allows users to improve & maintain the -software they use. This would help balance the rights of creators, users, and -the goals of preservation.

- -
- -
-
    -
  1. -

    I also played another pinball game called 3D Ultra Pinball, but that came - on a CD presumably for purchase. I have no idea whether my parents went - out and bought it, or if it came in a cereal box. (Yes, game CDs did show - up in cereal boxes sometimes.) ↩

    -
  2. -
-
- - -
- -
-
- - - -
-
- Legal • - RSS -

- Creative Commons License -

- Stephen Brennan's Blog is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License -
- -
- - - + + diff --git a/bin/unit_tests/assets/html/inline_block_wrap_files/88x31.png b/bin/unit_tests/assets/html/inline_block_wrap_files/88x31.png deleted file mode 100644 index 41edcfd50..000000000 Binary files a/bin/unit_tests/assets/html/inline_block_wrap_files/88x31.png and /dev/null differ diff --git a/src/eepp/ui/uihtmlwidget.cpp b/src/eepp/ui/uihtmlwidget.cpp index c2a56af62..f4abe2944 100644 --- a/src/eepp/ui/uihtmlwidget.cpp +++ b/src/eepp/ui/uihtmlwidget.cpp @@ -35,6 +35,11 @@ static std::string normalizeDataPropertyName( std::string_view name ) { return normalizedName; } +static bool isAtomicInlineAutoDisplay( CSSDisplay display ) { + return display == CSSDisplay::InlineBlock || display == CSSDisplay::InlineFlex || + display == CSSDisplay::InlineGrid; +} + static CSSBaselineAlignValue parseBaselineAlign( UIHTMLWidget* widget, const StyleSheetProperty& property ) { std::string_view val = property.value(); @@ -133,6 +138,21 @@ void UIHTMLWidget::setDisplay( CSSDisplay display ) { if ( oldDisplay == CSSDisplay::None ) setVisible( true ); + if ( isAtomicInlineAutoDisplay( mDisplay ) ) { + Sizef size( getPixelsSize() ); + bool resetSize = false; + if ( getLayoutWidthPolicy() == SizePolicy::WrapContent && size.getWidth() != 0.f ) { + size.setWidth( 0.f ); + resetSize = true; + } + if ( getLayoutHeightPolicy() == SizePolicy::WrapContent && size.getHeight() != 0.f ) { + size.setHeight( 0.f ); + resetSize = true; + } + if ( resetSize ) + setInternalPixelsSize( size ); + } + onDisplayChange(); } } diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index 4fbb8a9b4..cc960769d 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -1693,10 +1693,14 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri ( parentNode->asType()->isFlex() || parentNode->asType()->isGrid() ); if ( isInlineBlockTextSpan && mode == IntrinsicMode::None && + container->getLayoutWidthPolicy() != SizePolicy::WrapContent && container->getPixelsSize().getWidth() > 0 ) { maxWidth = container->getPixelsSize().getWidth() - container->getPixelsContentOffset().Left - container->getPixelsContentOffset().Right; + } else if ( isInlineBlockTextSpan && mode == IntrinsicMode::None && + container->getLayoutWidthPolicy() == SizePolicy::WrapContent ) { + maxWidth = 0; } else if ( parentIsFlexOrGrid && container->getLayoutWidthPolicy() == SizePolicy::WrapContent && mode == IntrinsicMode::None ) { diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp index 8f1adc934..e5c43b27b 100644 --- a/src/tests/unit_tests/uihtml_tests.cpp +++ b/src/tests/unit_tests/uihtml_tests.cpp @@ -2832,6 +2832,83 @@ UTEST( UIHTML, InlineBlockWrapIssue ) { Engine::destroySingleton(); } +UTEST( UIHTML, DeferredInlineBlockListDoesNotWrapItems ) { + Engine::instance()->createWindow( WindowSettings( 1024, 653, "Deferred Inline Block List Test", + WindowStyle::Default, WindowBackend::Default, + 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true ) ); + FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); + + auto* win = Engine::instance()->getCurrentWindow(); + UI::UISceneNode* sceneNode = init_test_inline_block(); + + UIWebView* webView = UIWebView::New(); + webView->setParent( sceneNode->getRoot() ); + webView->setPixelsSize( 1280, 600 ); + webView->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + webView->loadURI( URI( "./assets/html/inline_block_wrap_defer.html" ) ); + + UISceneNode* documentScene = webView->getDocumentSceneNode(); + ASSERT_TRUE( documentScene != nullptr ); + + UIWidget* ul = nullptr; + + for ( int i = 0; i < 100; i++ ) { + win->getInput()->update(); + SceneManager::instance()->update( Milliseconds( 16 ) ); + ul = documentScene->getRoot()->findByTag( "ul" ); + if ( ul ) { + auto lis = ul->findAllByTag( "li" ); + if ( !lis.empty() && + lis.front()->asType()->getDisplay() == CSSDisplay::InlineBlock ) + break; + } + Sys::sleep( Milliseconds( 1 ) ); + } + + ASSERT_TRUE( ul != nullptr ); + auto lis = ul->findAllByTag( "li" ); + ASSERT_EQ( lis.size(), (size_t)4 ); + + const Float rowY = lis.front()->getPixelsPosition().y; + Float previousRight = lis.front()->getPixelsPosition().x; + + for ( auto li : lis ) { + auto* htmlLi = li->asType(); + auto* richLi = li->asType(); + ASSERT_TRUE( htmlLi != nullptr ); + ASSERT_TRUE( richLi != nullptr ); + auto* anchor = li->findByTag( "a" ); + ASSERT_TRUE( anchor != nullptr ); + auto* htmlAnchor = anchor->asType(); + auto* richAnchor = anchor->asType(); + ASSERT_TRUE( htmlAnchor != nullptr ); + ASSERT_TRUE( richAnchor != nullptr ); + + EXPECT_EQ( htmlLi->getDisplay(), CSSDisplay::InlineBlock ); + EXPECT_EQ( htmlAnchor->getDisplay(), CSSDisplay::InlineBlock ); + EXPECT_EQ( richLi->getRichTextPtr()->getLines().size(), (size_t)1 ); + EXPECT_EQ( richAnchor->getRichTextPtr()->getLines().size(), (size_t)1 ); + const auto& anchorLine = richAnchor->getRichTextPtr()->getLines().front(); + const Rectf liContentOffset = li->asType()->getPixelsContentOffset(); + const Rectf anchorContentOffset = anchor->asType()->getPixelsContentOffset(); + EXPECT_NEAR( li->getPixelsSize().getHeight(), + anchorLine.height + liContentOffset.Top + liContentOffset.Bottom, 1.f ); + EXPECT_NEAR( anchor->getPixelsSize().getHeight(), + anchorLine.height + anchorContentOffset.Top + anchorContentOffset.Bottom, + 1.f ); + EXPECT_EQ( li->getPixelsPosition().y, rowY ); + EXPECT_GE( li->getPixelsPosition().x, previousRight ); + EXPECT_GT( li->getPixelsSize().getWidth(), 0 ); + EXPECT_GE( li->getPixelsSize().getWidth() + 1.f, li->getMaxIntrinsicWidth() ); + EXPECT_GE( anchor->getPixelsSize().getWidth() + 1.f, anchor->getMaxIntrinsicWidth() ); + + previousRight = li->getPixelsPosition().x + li->getPixelsSize().getWidth(); + } + + Engine::destroySingleton(); +} + UTEST( UIHTML, InlineBlockBrowserTest ) { Engine::instance()->createWindow( WindowSettings( 1024, 653, "Inline Block Browser Test", WindowStyle::Default, WindowBackend::Default, diff --git a/src/tests/unit_tests/uiwebview_tests.cpp b/src/tests/unit_tests/uiwebview_tests.cpp index 1457b5b29..7cff90a30 100644 --- a/src/tests/unit_tests/uiwebview_tests.cpp +++ b/src/tests/unit_tests/uiwebview_tests.cpp @@ -1504,7 +1504,7 @@ UTEST( UIWebView, RemoteImageIgnoredAfterNavigation ) { std::string imageData; const std::string imagePath = - Sys::getProcessPath() + "assets/html/inline_block_wrap_files/88x31.png"; + Sys::getProcessPath() + "assets/html/reddit_old_thread_files/pixel.png"; ASSERT_TRUE( FileSystem::fileGet( imagePath, imageData ) ); ASSERT_FALSE( imageData.empty() ); @@ -1614,7 +1614,7 @@ UTEST( UIWebView, RemoteBackgroundImageIgnoredAfterNavigation ) { std::string imageData; const std::string imagePath = - Sys::getProcessPath() + "assets/html/inline_block_wrap_files/88x31.png"; + Sys::getProcessPath() + "assets/html/reddit_old_thread_files/pixel.png"; ASSERT_TRUE( FileSystem::fileGet( imagePath, imageData ) ); ASSERT_FALSE( imageData.empty() ); @@ -2667,7 +2667,7 @@ UTEST( UIWebView, DestroyWithPendingSubresourcesIsSafe ) { std::string imageData; const std::string imagePath = - Sys::getProcessPath() + "assets/html/inline_block_wrap_files/88x31.png"; + Sys::getProcessPath() + "assets/html/reddit_old_thread_files/pixel.png"; ASSERT_TRUE( FileSystem::fileGet( imagePath, imageData ) ); ASSERT_FALSE( imageData.empty() );