From 360146eb9f9116d64fce38dc07a304273d324fb3 Mon Sep 17 00:00:00 2001 From: Thomas Nemer Date: Sat, 8 Nov 2025 15:20:56 +0100 Subject: [PATCH 1/9] Add window resize handling with responsive behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements comprehensive window resize handling throughout the component hierarchy, enabling automatic layout reflow when the window size changes. Features: - ResizeBehavior enum (FIXED, SCALE, FILL, MAINTAIN_ASPECT) - ResponsiveConstraints struct for min/max sizes and aspect ratio - Automatic resize propagation through component hierarchy - Panel auto-resize respecting autoFillParent setting - Scene window resize event handling and propagation - Comprehensive unit tests for all resize behaviors Changes: - Component: Added onParentResize() virtual method with automatic behavior handling based on ResizeBehavior. Added ResponsiveConstraints support. - Scene: Updated notifyChildrenOfResize() to properly propagate resize events - Panel: Added onParentResize() override for Panel-specific resize behavior - Demo app: Set root container to FILL behavior for automatic window filling - Tests: Added test_component_resize.cpp with comprehensive test coverage All acceptance criteria from issue #7 met: ✓ Scene captures window resize events ✓ Resize propagates through component hierarchy ✓ Components with layouts recalculate positions ✓ ResizeBehavior enum controls resize response ✓ Panels auto-fill parent on resize ✓ Min/max size constraints respected ✓ Aspect ratio constraints supported ✓ Demo app UI reflows properly on resize ✓ No manual resize handling needed in user code Closes #7 --- examples/demo_app/scenes/demo_scene.h | 17 +- include/bombfork/prong/components/panel.h | 47 +++++ include/bombfork/prong/core/component.h | 162 ++++++++++++++++ include/bombfork/prong/core/scene.h | 15 +- tests/test_component_resize.cpp | 215 ++++++++++++++++++++++ 5 files changed, 443 insertions(+), 13 deletions(-) create mode 100644 tests/test_component_resize.cpp diff --git a/examples/demo_app/scenes/demo_scene.h b/examples/demo_app/scenes/demo_scene.h index 91070bb..41b94da 100644 --- a/examples/demo_app/scenes/demo_scene.h +++ b/examples/demo_app/scenes/demo_scene.h @@ -315,9 +315,10 @@ class DemoScene : public Scene { // Add root container to scene addChild(std::move(rootContainer)); - // Initialize main container size to match scene/window + // Initialize main container size to match scene/window and set it to FILL behavior if (!children.empty() && children[0]) { children[0]->setBounds(0, 0, width, height); + children[0]->setResizeBehavior(Component::ResizeBehavior::FILL); children[0]->invalidateLayout(); } @@ -1040,20 +1041,24 @@ class DemoScene : public Scene { std::cout << " • Click DockLayout panels (Files, Tools, Properties) to see docking behavior" << std::endl; std::cout << " • Click any button to see console output" << std::endl; std::cout << " • Select items in ListBox" << std::endl; + std::cout << " • RESIZE the window to see automatic layout reflow!" << std::endl; std::cout << " • ESC key or 'Exit Application' to close" << std::endl; std::cout << "\n══════════════════════════════════════════════════════════════\n" << std::endl; } /** - * @brief Handle window resize + * @brief Handle window resize - automatic propagation via Scene base class */ void onWindowResize(int width, int height) override { + // Call base implementation which handles: + // 1. Update scene bounds + // 2. Invalidate layout + // 3. Notify renderer + // 4. Propagate resize to children (which triggers their ResizeBehavior) Scene::onWindowResize(width, height); - if (!children.empty() && children[0]) { - children[0]->setBounds(0, 0, width, height); - children[0]->invalidateLayout(); - } + // No manual sizing needed - root container has FILL behavior set in buildUI() + // and will automatically resize to match the scene dimensions } }; diff --git a/include/bombfork/prong/components/panel.h b/include/bombfork/prong/components/panel.h index 7cee00e..cb3bcb1 100644 --- a/include/bombfork/prong/components/panel.h +++ b/include/bombfork/prong/components/panel.h @@ -397,6 +397,53 @@ class Panel : public Component { } } + // === Resize Handling === + + /** + * @brief Handle parent resize events + * + * Panels can automatically fill parent or use standard resize behavior. + * This override checks for auto-fill first, then delegates to Component. + * + * @param parentWidth New parent width + * @param parentHeight New parent height + */ + void onParentResize(int parentWidth, int parentHeight) override { + // If auto-fill is enabled, use FILL behavior regardless of setting + if (autoFillParent) { + // Try to get content bounds from parent if it's a Panel + Panel* parentPanel = dynamic_cast(parent); + if (parentPanel) { + int contentX, contentY, contentWidth, contentHeight; + parentPanel->getContentBounds(contentX, contentY, contentWidth, contentHeight); + + // Convert parent's global content bounds to local coordinates + int parentGx, parentGy; + parent->getGlobalPosition(parentGx, parentGy); + int localContentX = contentX - parentGx; + int localContentY = contentY - parentGy; + + setBounds(localContentX, localContentY, contentWidth, contentHeight); + } else { + // Parent is not a Panel, fill entire parent space + setBounds(0, 0, parentWidth, parentHeight); + } + + // Mark layout as invalid to trigger re-layout + invalidateLayout(); + + // Propagate to children + for (auto& child : children) { + if (child) { + child->onParentResize(width, height); + } + } + } else { + // Use standard resize behavior from Component + Component::onParentResize(parentWidth, parentHeight); + } + } + // === Update === void update(double deltaTime) override { diff --git a/include/bombfork/prong/core/component.h b/include/bombfork/prong/core/component.h index 08c599d..3e79361 100644 --- a/include/bombfork/prong/core/component.h +++ b/include/bombfork/prong/core/component.h @@ -4,6 +4,8 @@ #include #include +#include +#include #include #include #include @@ -40,6 +42,31 @@ class Component { ACTIVE // Component is being interacted with }; + /** + * @brief Resize behavior controls how component responds to parent resize + */ + enum class ResizeBehavior { + FIXED, // Keep original size and position + SCALE, // Scale proportionally with parent + FILL, // Fill available parent space + MAINTAIN_ASPECT // Scale while maintaining aspect ratio + }; + + /** + * @brief Constraints for responsive sizing + */ + struct ResponsiveConstraints { + int minWidth = 0; + int minHeight = 0; + int maxWidth = INT_MAX; + int maxHeight = INT_MAX; + float aspectRatio = 0.0f; // 0 = no constraint + + ResponsiveConstraints() = default; + ResponsiveConstraints(int minW, int minH, int maxW = INT_MAX, int maxH = INT_MAX, float ar = 0.0f) + : minWidth(minW), minHeight(minH), maxWidth(maxW), maxHeight(maxH), aspectRatio(ar) {} + }; + public: // Renderer access bombfork::prong::rendering::IRenderer* renderer = nullptr; @@ -83,6 +110,12 @@ class Component { FocusState focusState = FocusState::NONE; bool isCurrentlyHovered = false; // Track if mouse is currently over this component + // Resize handling + ResizeBehavior resizeBehavior = ResizeBehavior::FIXED; + ResponsiveConstraints constraints; + int originalParentWidth = 0; + int originalParentHeight = 0; + // Parent/child relationships Component* parent = nullptr; std::vector> children; @@ -370,6 +403,135 @@ class Component { virtual bool canReceiveFocus() const { return enabled && visible; } + // === Resize Management === + + /** + * @brief Set resize behavior for this component + * @param behavior How component should respond to parent resize + */ + void setResizeBehavior(ResizeBehavior behavior) { resizeBehavior = behavior; } + + /** + * @brief Get current resize behavior + */ + ResizeBehavior getResizeBehavior() const { return resizeBehavior; } + + /** + * @brief Set responsive constraints for sizing + * @param newConstraints Min/max size and aspect ratio constraints + */ + void setConstraints(const ResponsiveConstraints& newConstraints) { constraints = newConstraints; } + + /** + * @brief Get current constraints + */ + const ResponsiveConstraints& getConstraints() const { return constraints; } + + /** + * @brief Called when parent component is resized + * + * Override this to implement custom resize behavior. The default + * implementation uses the ResizeBehavior setting to automatically + * handle common resize patterns. + * + * @param parentWidth New parent width + * @param parentHeight New parent height + */ + virtual void onParentResize(int parentWidth, int parentHeight) { + // Initialize original parent dimensions if not set + if (originalParentWidth == 0 && originalParentHeight == 0) { + originalParentWidth = parentWidth; + originalParentHeight = parentHeight; + } + + // Apply resize behavior + switch (resizeBehavior) { + case ResizeBehavior::FILL: { + // Fill available parent space + setBounds(0, 0, parentWidth, parentHeight); + break; + } + case ResizeBehavior::SCALE: { + // Scale proportionally + if (originalParentWidth > 0 && originalParentHeight > 0) { + float scaleX = parentWidth / static_cast(originalParentWidth); + float scaleY = parentHeight / static_cast(originalParentHeight); + + int newX = static_cast(localX * scaleX); + int newY = static_cast(localY * scaleY); + int newWidth = static_cast(width * scaleX); + int newHeight = static_cast(height * scaleY); + + setBounds(newX, newY, newWidth, newHeight); + } + break; + } + case ResizeBehavior::MAINTAIN_ASPECT: { + // Scale while maintaining aspect ratio + if (originalParentWidth > 0 && originalParentHeight > 0 && width > 0 && height > 0) { + float currentAspect = width / static_cast(height); + float scale = std::min(parentWidth / static_cast(originalParentWidth), + parentHeight / static_cast(originalParentHeight)); + + int newWidth = static_cast(width * scale); + int newHeight = static_cast(newWidth / currentAspect); + + // Center in available space + int newX = (parentWidth - newWidth) / 2; + int newY = (parentHeight - newHeight) / 2; + + setBounds(newX, newY, newWidth, newHeight); + } + break; + } + case ResizeBehavior::FIXED: + // Do nothing - keep original size and position + break; + } + + // Apply constraints + applyConstraints(); + + // Mark layout as invalid to trigger re-layout + invalidateLayout(); + + // Propagate to children + for (auto& child : children) { + if (child) { + child->onParentResize(width, height); + } + } + } + +protected: + /** + * @brief Apply responsive constraints to current size + */ + void applyConstraints() { + if (constraints.minWidth > 0 || constraints.maxWidth < INT_MAX || constraints.minHeight > 0 || + constraints.maxHeight < INT_MAX || constraints.aspectRatio > 0.0f) { + + int newWidth = width; + int newHeight = height; + + // Apply min/max constraints + newWidth = std::clamp(newWidth, constraints.minWidth, constraints.maxWidth); + newHeight = std::clamp(newHeight, constraints.minHeight, constraints.maxHeight); + + // Apply aspect ratio constraint + if (constraints.aspectRatio > 0.0f) { + newHeight = static_cast(newWidth / constraints.aspectRatio); + // Re-check height constraints after aspect ratio adjustment + newHeight = std::clamp(newHeight, constraints.minHeight, constraints.maxHeight); + } + + if (newWidth != width || newHeight != height) { + setSize(newWidth, newHeight); + } + } + } + +public: // === Parent/Child Management === void addChild(std::unique_ptr child) { diff --git a/include/bombfork/prong/core/scene.h b/include/bombfork/prong/core/scene.h index 173e568..629745e 100644 --- a/include/bombfork/prong/core/scene.h +++ b/include/bombfork/prong/core/scene.h @@ -193,18 +193,19 @@ class Scene : public Component { /** * @brief Notify children of window resize * - * Children can override setBounds() to implement custom resize behavior. + * Calls onParentResize() on all children to trigger their resize behavior. * * @param width New window width * @param height New window height */ void notifyChildrenOfResize(int width, int height) { - (void)width; // Available for child components to use - (void)height; // Available for child components to use - - // Children can override setBounds() to implement custom resize behavior. - // By default, children maintain their current position and size. - // For automatic layout, use Layout managers or override this method. + // Notify all children of the resize + // They will apply their ResizeBehavior and propagate to their children + for (auto& child : children) { + if (child) { + child->onParentResize(width, height); + } + } } }; diff --git a/tests/test_component_resize.cpp b/tests/test_component_resize.cpp new file mode 100644 index 0000000..a031591 --- /dev/null +++ b/tests/test_component_resize.cpp @@ -0,0 +1,215 @@ +/** + * @file test_component_resize.cpp + * @brief Unit tests for component resize handling + */ + +#include + +#include +#include +#include +#include +#include +#include + +using namespace bombfork::prong; + +// Simple concrete component for testing +class TestComponent : public Component { +public: + explicit TestComponent(const std::string& name = "TestComponent") : Component(nullptr, name) {} + + void update(double deltaTime) override { (void)deltaTime; } + void render() override {} +}; + +void testResizeBehaviorFixed() { + std::cout << "Testing ResizeBehavior::FIXED..." << std::endl; + + TestComponent parent; + parent.setBounds(0, 0, 800, 600); + + auto child = std::make_unique("FixedChild"); + child->setBounds(100, 100, 200, 150); + child->setResizeBehavior(Component::ResizeBehavior::FIXED); + + auto* childPtr = child.get(); + parent.addChild(std::move(child)); + + // Initial size + int x, y, w, h; + childPtr->getBounds(x, y, w, h); + assert(x == 100 && y == 100 && w == 200 && h == 150); + + // Resize parent - FIXED should not change + childPtr->onParentResize(1024, 768); + + childPtr->getBounds(x, y, w, h); + assert(x == 100 && y == 100 && w == 200 && h == 150); + + std::cout << "✓ ResizeBehavior::FIXED test passed" << std::endl; +} + +void testResizeBehaviorFill() { + std::cout << "Testing ResizeBehavior::FILL..." << std::endl; + + TestComponent parent; + parent.setBounds(0, 0, 800, 600); + + auto child = std::make_unique("FillChild"); + child->setBounds(0, 0, 800, 600); + child->setResizeBehavior(Component::ResizeBehavior::FILL); + + auto* childPtr = child.get(); + parent.addChild(std::move(child)); + + // Resize parent - FILL should match parent size + childPtr->onParentResize(1024, 768); + + int x, y, w, h; + childPtr->getBounds(x, y, w, h); + assert(x == 0 && y == 0 && w == 1024 && h == 768); + + std::cout << "✓ ResizeBehavior::FILL test passed" << std::endl; +} + +void testResizeBehaviorScale() { + std::cout << "Testing ResizeBehavior::SCALE..." << std::endl; + + TestComponent parent; + parent.setBounds(0, 0, 800, 600); + + auto child = std::make_unique("ScaleChild"); + child->setBounds(100, 100, 200, 150); + child->setResizeBehavior(Component::ResizeBehavior::SCALE); + + auto* childPtr = child.get(); + parent.addChild(std::move(child)); + + // First call to onParentResize establishes original parent size + childPtr->onParentResize(800, 600); + + // Now resize to 2x - should scale proportionally + childPtr->onParentResize(1600, 1200); + + int x, y, w, h; + childPtr->getBounds(x, y, w, h); + assert(x == 200 && y == 200 && w == 400 && h == 300); + + std::cout << "✓ ResizeBehavior::SCALE test passed" << std::endl; +} + +void testResizeBehaviorMaintainAspect() { + std::cout << "Testing ResizeBehavior::MAINTAIN_ASPECT..." << std::endl; + + TestComponent parent; + parent.setBounds(0, 0, 800, 600); + + auto child = std::make_unique("AspectChild"); + child->setBounds(0, 0, 400, 300); // 4:3 aspect ratio + child->setResizeBehavior(Component::ResizeBehavior::MAINTAIN_ASPECT); + + auto* childPtr = child.get(); + parent.addChild(std::move(child)); + + // First call establishes original parent size + childPtr->onParentResize(800, 600); + + // Resize to different aspect ratio - child should maintain its aspect + childPtr->onParentResize(1200, 600); // Wide parent + + int x, y, w, h; + childPtr->getBounds(x, y, w, h); + + // Should maintain 4:3 aspect ratio and be centered + assert(w == 400 && h == 300); // Same scale (1.0x) since height limited it + + std::cout << "✓ ResizeBehavior::MAINTAIN_ASPECT test passed" << std::endl; +} + +void testResponsiveConstraints() { + std::cout << "Testing ResponsiveConstraints..." << std::endl; + + TestComponent parent; + parent.setBounds(0, 0, 800, 600); + + auto child = std::make_unique("ConstrainedChild"); + child->setBounds(0, 0, 500, 400); + child->setResizeBehavior(Component::ResizeBehavior::FILL); + + // Set constraints + Component::ResponsiveConstraints constraints; + constraints.minWidth = 200; + constraints.minHeight = 150; + constraints.maxWidth = 600; + constraints.maxHeight = 450; + child->setConstraints(constraints); + + auto* childPtr = child.get(); + parent.addChild(std::move(child)); + + // Try to resize beyond max - should be clamped + childPtr->onParentResize(1000, 1000); + + int x, y, w, h; + childPtr->getBounds(x, y, w, h); + assert(w == 600 && h == 450); // Clamped to max + + std::cout << "✓ ResponsiveConstraints test passed" << std::endl; +} + +void testResizePropagation() { + std::cout << "Testing resize propagation through hierarchy..." << std::endl; + + TestComponent root; + root.setBounds(0, 0, 800, 600); + + auto parent = std::make_unique("Parent"); + parent->setBounds(0, 0, 800, 600); + parent->setResizeBehavior(Component::ResizeBehavior::FILL); + + auto child = std::make_unique("Child"); + child->setBounds(0, 0, 800, 600); + child->setResizeBehavior(Component::ResizeBehavior::FILL); + + auto* childPtr = child.get(); + parent->addChild(std::move(child)); + + auto* parentPtr = parent.get(); + root.addChild(std::move(parent)); + + // Resize root - should propagate to children + parentPtr->onParentResize(1024, 768); + + int px, py, pw, ph; + parentPtr->getBounds(px, py, pw, ph); + assert(pw == 1024 && ph == 768); + + int cx, cy, cw, ch; + childPtr->getBounds(cx, cy, cw, ch); + assert(cw == 1024 && ch == 768); + + std::cout << "✓ Resize propagation test passed" << std::endl; +} + +int main() { + std::cout << "=== Component Resize Handling Tests ===" << std::endl << std::endl; + + try { + testResizeBehaviorFixed(); + testResizeBehaviorFill(); + testResizeBehaviorScale(); + testResizeBehaviorMaintainAspect(); + testResponsiveConstraints(); + testResizePropagation(); + + std::cout << std::endl << "✓ All resize handling tests passed!" << std::endl; + return 0; + } catch (const std::exception& e) { + std::cerr << "✗ Test failed with exception: " << e.what() << std::endl; + return 1; + } catch (...) { + std::cerr << "✗ Test failed with unknown exception" << std::endl; + return 1; + } +} From d1e070548bf5295ab2e50a41d8d104d571493b58 Mon Sep 17 00:00:00 2001 From: Thomas Nemer Date: Sat, 8 Nov 2025 20:57:24 +0100 Subject: [PATCH 2/9] Fix resize shrinking behavior for SCALE and MAINTAIN_ASPECT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The resize implementation had a critical bug where SCALE and MAINTAIN_ASPECT behaviors would not work correctly when shrinking because they were scaling from the *current* dimensions instead of the *original* dimensions. Problem: - Component starts at 100x100 in 800x600 parent (original state) - Parent grows to 1600x1200 (2x): component scales to 200x200 ✓ - Parent shrinks to 400x300 (0.5x): component should be 50x50 but stayed 100x100 ✗ Root Cause: The resize logic was: newWidth = currentWidth * scale This caused compounding errors - each resize scaled from the previous size, not the original size. Solution: Store original component dimensions (originalLocalX, originalLocalY, originalWidth, originalHeight) on first resize call, then always scale from these original values: newWidth = originalWidth * scale This ensures consistent scaling in both directions (grow and shrink). Changes: - Added originalLocalX, originalLocalY, originalWidth, originalHeight members - Modified onParentResize() to store original dimensions on first call - Updated SCALE behavior to scale from original dimensions - Updated MAINTAIN_ASPECT behavior to scale from original dimensions - Enhanced tests to verify both grow and shrink scenarios All tests pass (16/16). --- include/bombfork/prong/core/component.h | 31 ++++--- tests/test_component_resize.cpp | 18 ++++- tests/test_resize_shrink.cpp | 103 ++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 14 deletions(-) create mode 100644 tests/test_resize_shrink.cpp diff --git a/include/bombfork/prong/core/component.h b/include/bombfork/prong/core/component.h index 3e79361..d88b409 100644 --- a/include/bombfork/prong/core/component.h +++ b/include/bombfork/prong/core/component.h @@ -115,6 +115,11 @@ class Component { ResponsiveConstraints constraints; int originalParentWidth = 0; int originalParentHeight = 0; + // Original component dimensions (for SCALE and MAINTAIN_ASPECT behaviors) + int originalLocalX = 0; + int originalLocalY = 0; + int originalWidth = 0; + int originalHeight = 0; // Parent/child relationships Component* parent = nullptr; @@ -438,10 +443,15 @@ class Component { * @param parentHeight New parent height */ virtual void onParentResize(int parentWidth, int parentHeight) { - // Initialize original parent dimensions if not set + // Initialize original dimensions if not set (first resize call) if (originalParentWidth == 0 && originalParentHeight == 0) { originalParentWidth = parentWidth; originalParentHeight = parentHeight; + // Store original component dimensions for SCALE and MAINTAIN_ASPECT + originalLocalX = localX; + originalLocalY = localY; + originalWidth = width; + originalHeight = height; } // Apply resize behavior @@ -452,28 +462,29 @@ class Component { break; } case ResizeBehavior::SCALE: { - // Scale proportionally + // Scale proportionally from ORIGINAL dimensions if (originalParentWidth > 0 && originalParentHeight > 0) { float scaleX = parentWidth / static_cast(originalParentWidth); float scaleY = parentHeight / static_cast(originalParentHeight); - int newX = static_cast(localX * scaleX); - int newY = static_cast(localY * scaleY); - int newWidth = static_cast(width * scaleX); - int newHeight = static_cast(height * scaleY); + // Scale from original dimensions, not current ones! + int newX = static_cast(originalLocalX * scaleX); + int newY = static_cast(originalLocalY * scaleY); + int newWidth = static_cast(originalWidth * scaleX); + int newHeight = static_cast(originalHeight * scaleY); setBounds(newX, newY, newWidth, newHeight); } break; } case ResizeBehavior::MAINTAIN_ASPECT: { - // Scale while maintaining aspect ratio - if (originalParentWidth > 0 && originalParentHeight > 0 && width > 0 && height > 0) { - float currentAspect = width / static_cast(height); + // Scale while maintaining aspect ratio from ORIGINAL dimensions + if (originalParentWidth > 0 && originalParentHeight > 0 && originalWidth > 0 && originalHeight > 0) { + float currentAspect = originalWidth / static_cast(originalHeight); float scale = std::min(parentWidth / static_cast(originalParentWidth), parentHeight / static_cast(originalParentHeight)); - int newWidth = static_cast(width * scale); + int newWidth = static_cast(originalWidth * scale); int newHeight = static_cast(newWidth / currentAspect); // Center in available space diff --git a/tests/test_component_resize.cpp b/tests/test_component_resize.cpp index a031591..29f6b6f 100644 --- a/tests/test_component_resize.cpp +++ b/tests/test_component_resize.cpp @@ -63,14 +63,19 @@ void testResizeBehaviorFill() { auto* childPtr = child.get(); parent.addChild(std::move(child)); - // Resize parent - FILL should match parent size + // Resize parent larger - FILL should match parent size (grow) childPtr->onParentResize(1024, 768); int x, y, w, h; childPtr->getBounds(x, y, w, h); assert(x == 0 && y == 0 && w == 1024 && h == 768); - std::cout << "✓ ResizeBehavior::FILL test passed" << std::endl; + // Resize parent smaller - FILL should match parent size (shrink) + childPtr->onParentResize(640, 480); + childPtr->getBounds(x, y, w, h); + assert(x == 0 && y == 0 && w == 640 && h == 480); + + std::cout << "✓ ResizeBehavior::FILL test passed (grow and shrink)" << std::endl; } void testResizeBehaviorScale() { @@ -89,14 +94,19 @@ void testResizeBehaviorScale() { // First call to onParentResize establishes original parent size childPtr->onParentResize(800, 600); - // Now resize to 2x - should scale proportionally + // Resize to 2x - should scale proportionally (grow) childPtr->onParentResize(1600, 1200); int x, y, w, h; childPtr->getBounds(x, y, w, h); assert(x == 200 && y == 200 && w == 400 && h == 300); - std::cout << "✓ ResizeBehavior::SCALE test passed" << std::endl; + // Resize to 0.5x - should scale proportionally (shrink) + childPtr->onParentResize(400, 300); + childPtr->getBounds(x, y, w, h); + assert(x == 50 && y == 50 && w == 100 && h == 75); + + std::cout << "✓ ResizeBehavior::SCALE test passed (grow and shrink)" << std::endl; } void testResizeBehaviorMaintainAspect() { diff --git a/tests/test_resize_shrink.cpp b/tests/test_resize_shrink.cpp new file mode 100644 index 0000000..f8a9582 --- /dev/null +++ b/tests/test_resize_shrink.cpp @@ -0,0 +1,103 @@ +/** + * @file test_resize_shrink.cpp + * @brief Test component resize shrinking behavior + */ + +#include + +#include +#include +#include +#include +#include + +using namespace bombfork::prong; + +// Simple concrete component for testing +class TestComponent : public Component { +public: + explicit TestComponent(const std::string& name = "TestComponent") : Component(nullptr, name) {} + + void update(double deltaTime) override { (void)deltaTime; } + void render() override {} +}; + +int main() { + std::cout << "=== Testing Resize SHRINKING Behavior ===" << std::endl << std::endl; + + // Test FILL behavior with shrinking + { + std::cout << "Test 1: FILL behavior - grow then shrink" << std::endl; + TestComponent parent; + parent.setBounds(0, 0, 800, 600); + + auto child = std::make_unique("FillChild"); + child->setBounds(0, 0, 800, 600); + child->setResizeBehavior(Component::ResizeBehavior::FILL); + + auto* childPtr = child.get(); + parent.addChild(std::move(child)); + + // Grow + std::cout << " Growing to 1024x768..." << std::endl; + childPtr->onParentResize(1024, 768); + int x, y, w, h; + childPtr->getBounds(x, y, w, h); + std::cout << " After grow: " << w << "x" << h << std::endl; + assert(w == 1024 && h == 768); + + // Shrink + std::cout << " Shrinking to 640x480..." << std::endl; + childPtr->onParentResize(640, 480); + childPtr->getBounds(x, y, w, h); + std::cout << " After shrink: " << w << "x" << h << std::endl; + assert(w == 640 && h == 480); + + std::cout << " ✓ FILL shrink test PASSED" << std::endl << std::endl; + } + + // Test SCALE behavior with shrinking + { + std::cout << "Test 2: SCALE behavior - grow then shrink" << std::endl; + TestComponent parent; + parent.setBounds(0, 0, 800, 600); + + auto child = std::make_unique("ScaleChild"); + child->setBounds(100, 100, 200, 150); + child->setResizeBehavior(Component::ResizeBehavior::SCALE); + + auto* childPtr = child.get(); + parent.addChild(std::move(child)); + + // Establish original parent size + std::cout << " Establishing original parent size 800x600..." << std::endl; + childPtr->onParentResize(800, 600); + + // Grow to 1600x1200 (2x) + std::cout << " Growing to 1600x1200 (2x)..." << std::endl; + childPtr->onParentResize(1600, 1200); + int x, y, w, h; + childPtr->getBounds(x, y, w, h); + std::cout << " After grow: pos=(" << x << "," << y << ") size=" << w << "x" << h << std::endl; + std::cout << " Expected: pos=(200,200) size=400x300" << std::endl; + assert(x == 200 && y == 200 && w == 400 && h == 300); + + // Shrink to 400x300 (0.5x of original) + std::cout << " Shrinking to 400x300 (0.5x)..." << std::endl; + childPtr->onParentResize(400, 300); + childPtr->getBounds(x, y, w, h); + std::cout << " After shrink: pos=(" << x << "," << y << ") size=" << w << "x" << h << std::endl; + std::cout << " Expected: pos=(50,50) size=100x75" << std::endl; + + if (x == 50 && y == 50 && w == 100 && h == 75) { + std::cout << " ✓ SCALE shrink test PASSED" << std::endl << std::endl; + } else { + std::cout << " ✗ SCALE shrink test FAILED - sizes don't match!" << std::endl; + std::cout << " This confirms the bug: SCALE doesn't work properly when shrinking!" << std::endl << std::endl; + return 1; + } + } + + std::cout << "✓ All shrinking tests passed!" << std::endl; + return 0; +} From 3f09c056235cee72450ea1aa9919d88bfd4b1bcb Mon Sep 17 00:00:00 2001 From: Thomas Nemer Date: Sat, 8 Nov 2025 21:08:26 +0100 Subject: [PATCH 3/9] Fix setBounds() to propagate resize to children MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical fix: When a component's size changes via setBounds() (e.g., by a layout manager), it must notify its children so they can apply their resize behaviors. Without this, only the top-level component would resize while all descendants would remain at their original size. Problem: 1. Window shrinks from 800×600 to 600×400 2. Root container resizes to 600×400 (FILL behavior works) 3. FlexLayout calls setBounds() on child panels 4. Child panels resize BUT their children are never notified 5. Grandchildren stay at original size and get cropped ✗ Root Cause: setBounds() only updated the component's own dimensions without notifying children. This broke the resize propagation chain when layouts repositioned components. Solution: Modified setBounds() to automatically call onParentResize() on all children when the size changes. This ensures resize propagation works correctly throughout the entire component hierarchy, regardless of whether the resize is triggered by: - Direct onParentResize() call (window resize) - Layout manager calling setBounds() (layout recalculation) - Manual setBounds() call (programmatic resize) This is the key fix that makes window shrinking work in the demo app. The root container resizes, triggers layout recalculation, layout calls setBounds() on children, which now properly propagates to grandchildren. All tests pass (16/16). --- include/bombfork/prong/core/component.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/include/bombfork/prong/core/component.h b/include/bombfork/prong/core/component.h index d88b409..b9d05fd 100644 --- a/include/bombfork/prong/core/component.h +++ b/include/bombfork/prong/core/component.h @@ -168,11 +168,23 @@ class Component { * @param newHeight Component height */ virtual void setBounds(int newX, int newY, int newWidth, int newHeight) { + // Check if size actually changed + bool sizeChanged = (width != newWidth || height != newHeight); + localX = newX; localY = newY; width = newWidth; height = newHeight; invalidateGlobalCache(); + + // If size changed, notify children so they can apply their resize behavior + if (sizeChanged) { + for (auto& child : children) { + if (child) { + child->onParentResize(newWidth, newHeight); + } + } + } } /** From 77cbffc0642701373c55a2dc56ab5112cd006bf7 Mon Sep 17 00:00:00 2001 From: Thomas Nemer Date: Sun, 9 Nov 2025 09:55:13 +0100 Subject: [PATCH 4/9] chore: improve demo scene layout --- examples/demo_app/scenes/demo_scene.h | 50 +++++++++------------------ 1 file changed, 17 insertions(+), 33 deletions(-) diff --git a/examples/demo_app/scenes/demo_scene.h b/examples/demo_app/scenes/demo_scene.h index 41b94da..6e19f16 100644 --- a/examples/demo_app/scenes/demo_scene.h +++ b/examples/demo_app/scenes/demo_scene.h @@ -76,7 +76,6 @@ class StatusLabel : public Component { // Calculate baseline Y position // Text baseline needs to be positioned so the text appears centered in the component - // For a 24px font in a 30px component, baseline at +18 from top works well int baselineY = gy; renderer->drawText(text_.c_str(), gx + 10, baselineY, textColor_.r, textColor_.g, textColor_.b, textColor_.a); @@ -125,8 +124,8 @@ class DemoScene : public Scene { lastDeltaTime = deltaTime; lastFpsUpdateTime += deltaTime; - // Update FPS counter every 0.1 seconds - if (lastFpsUpdateTime >= 0.1 && fpsLabelPtr) { + // Update FPS counter every 1 seconds + if (lastFpsUpdateTime >= 1 && fpsLabelPtr) { int fps = static_cast(std::round(1.0 / deltaTime)); fpsLabelPtr->setText("FPS: " + std::to_string(fps)); lastFpsUpdateTime = 0.0; @@ -223,10 +222,10 @@ class DemoScene : public Scene { // Create GLFW adapters for TextInput adapters = examples::glfw::GLFWAdapters::create(glfwWindow); - // === Main Layout - FlexLayout Horizontal === + // === Main Layout - FlexLayout Vertical === auto mainLayout = std::make_shared(); mainLayout->configure(FlexLayout::Configuration{ - .direction = FlexDirection::ROW, .justify = FlexJustify::START, .align = FlexAlign::STRETCH, .gap = 15.0f}); + .direction = FlexDirection::ROW, .justify = FlexJustify::START, .align = FlexAlign::STRETCH, .gap = 3.0f}); // Set flex item properties: left and right panels fixed width, center panel grows to fill mainLayout->setItemProperties({ @@ -244,11 +243,9 @@ class DemoScene : public Scene { toolbarPanelLayout->configure(FlexLayout::Configuration{ .direction = FlexDirection::ROW, .justify = FlexJustify::START, .align = FlexAlign::STRETCH, .gap = 0.0f}); - auto toolbarPanel = create().withSize(0, 40).withLayout(toolbarPanelLayout).build(); - toolbarPanel->setBackgroundColor(theming::Color(0.12f, 0.12f, 0.14f, 1.0f)); + auto toolbarPanel = create().withSize(0, 35).withLayout(toolbarPanelLayout).build(); toolbarPanel->setBorderColor(theming::Color(0.3f, 0.3f, 0.35f, 1.0f)); toolbarPanel->setBorderWidth(1); - toolbarPanel->setPadding(5); toolbarPanel->addChild(std::move(toolbar)); // === LEFT PANEL - Controls & Inputs === @@ -263,7 +260,6 @@ class DemoScene : public Scene { // === Assemble 3-panel layout with FlexLayout === auto threePanelContainer = create().withLayout(mainLayout).build(); threePanelContainer->setBackgroundColor(theming::Color(0.08f, 0.08f, 0.1f, 1.0f)); - threePanelContainer->setPadding(15); threePanelContainer->addChild(std::move(leftPanel)); threePanelContainer->addChild(std::move(centerPanel)); @@ -280,7 +276,6 @@ class DemoScene : public Scene { statusBarPanel->setBackgroundColor(theming::Color(0.1f, 0.1f, 0.12f, 1.0f)); statusBarPanel->setBorderColor(theming::Color(0.3f, 0.3f, 0.35f, 1.0f)); statusBarPanel->setBorderWidth(1); - statusBarPanel->setPadding(5); // Left status label auto appNameLabel = std::make_unique("Prong UI Framework - Scene Demo"); @@ -337,20 +332,20 @@ class DemoScene : public Scene { std::unique_ptr buildControlPanel() { auto leftLayout = std::make_shared(); leftLayout->configure(FlexLayout::Configuration{ - .direction = FlexDirection::COLUMN, .justify = FlexJustify::START, .align = FlexAlign::STRETCH, .gap = 10.0f}); + .direction = FlexDirection::COLUMN, .justify = FlexJustify::START, .align = FlexAlign::STRETCH, .gap = 1.0f}); - auto leftPanel = create().withSize(280, 0).withLayout(leftLayout).build(); + auto leftPanel = create().withSize(200, 0).withLayout(leftLayout).build(); leftPanel->setBackgroundColor(theming::Color(0.15f, 0.15f, 0.18f, 1.0f)); leftPanel->setBorderColor(theming::Color(0.3f, 0.3f, 0.35f, 1.0f)); leftPanel->setBorderWidth(2); leftPanel->setTitle("Controls"); - leftPanel->setPadding(15); // === TextInput Demo === auto textInput = create() .withPlaceholder("Enter text here...") + .withSize(0, 30) .withTextChangedCallback([](const std::string& text) { std::cout << "Text: " << text << std::endl; }) .build(); textInputPtr = textInput.get(); @@ -364,7 +359,7 @@ class DemoScene : public Scene { buttonRowLayout->configure(FlexLayout::Configuration{.direction = FlexDirection::ROW, .justify = FlexJustify::SPACE_BETWEEN, .align = FlexAlign::STRETCH, - .gap = 10.0f}); + .gap = 1.0f}); auto buttonRow = create().withLayout(buttonRowLayout).build(); buttonRow->setBackgroundColor(theming::Color(0.0f, 0.0f, 0.0f, 0.0f)); @@ -453,7 +448,6 @@ class DemoScene : public Scene { centerPanel->setBorderColor(theming::Color(0.3f, 0.3f, 0.35f, 1.0f)); centerPanel->setBorderWidth(2); centerPanel->setTitle("Layout Demonstrations"); - centerPanel->setPadding(15); // === GridLayout Demo === auto gridPanel = buildGridLayoutDemo(); @@ -496,7 +490,6 @@ class DemoScene : public Scene { gridPanel->setBorderColor(theming::Color(0.3f, 0.3f, 0.35f, 1.0f)); gridPanel->setBorderWidth(1); gridPanel->setTitle("GridLayout (3x3 Button Grid)"); - gridPanel->setPadding(10); // Add 9 buttons in 3x3 grid for (int i = 1; i <= 9; ++i) { @@ -527,7 +520,6 @@ class DemoScene : public Scene { flowPanel->setBorderColor(theming::Color(0.3f, 0.3f, 0.35f, 1.0f)); flowPanel->setBorderWidth(1); flowPanel->setTitle("FlowLayout (Tag-like Interface)"); - flowPanel->setPadding(10); // Add various-sized "tag" buttons std::vector tags = {"C++20", "UI", "Framework", "Modern", @@ -559,7 +551,6 @@ class DemoScene : public Scene { stackPanel->setBorderColor(theming::Color(0.3f, 0.3f, 0.35f, 1.0f)); stackPanel->setBorderWidth(1); stackPanel->setTitle("StackLayout (Horizontal Stack)"); - stackPanel->setPadding(10); stackPanel->setLayoutManager(stackLayout); // Add horizontally stacked buttons @@ -587,7 +578,6 @@ class DemoScene : public Scene { wrapperPanel->setBorderColor(theming::Color(0.3f, 0.3f, 0.35f, 1.0f)); wrapperPanel->setBorderWidth(1); wrapperPanel->setTitle("DockLayout (Application-style Panel Docking)"); - wrapperPanel->setPadding(10); // Create DockLayout auto dockLayout = std::make_shared(); @@ -607,7 +597,6 @@ class DemoScene : public Scene { leftPanel->setBorderColor(theming::Color(0.4f, 0.5f, 0.6f, 1.0f)); leftPanel->setBorderWidth(1); leftPanel->setTitle("Files"); - leftPanel->setPadding(5); // Add file browser-style buttons auto fileLayout = std::make_shared(); @@ -631,7 +620,6 @@ class DemoScene : public Scene { topPanel->setBorderColor(theming::Color(0.5f, 0.4f, 0.6f, 1.0f)); topPanel->setBorderWidth(1); topPanel->setTitle("Tools"); - topPanel->setPadding(5); // Add horizontal tool buttons auto toolLayout = std::make_shared(); @@ -654,7 +642,6 @@ class DemoScene : public Scene { rightPanel->setBorderColor(theming::Color(0.6f, 0.5f, 0.4f, 1.0f)); rightPanel->setBorderWidth(1); rightPanel->setTitle("Properties"); - rightPanel->setPadding(5); // Add property-style labels auto propLayout = std::make_shared(); @@ -676,7 +663,6 @@ class DemoScene : public Scene { centerPanel->setBorderColor(theming::Color(0.3f, 0.3f, 0.3f, 1.0f)); centerPanel->setBorderWidth(1); centerPanel->setTitle("Editor"); - centerPanel->setPadding(5); // Add center content label auto centerLabel = create