ResizableLayout for stretching and moving Components


#1

In the spirit of Christmas, I want to share this class I wrote for giving child Components the ability to stretch and move around when the parent component is resized. Sure, I know about juce::StretchableLayoutManager but this one works a bit differently (and in 2 dimensions).

To make a component stick to the right of your window its as easy as

addToLayout (component, anchorTopRight);

To make the bottom right corner of a component stretch with the parent, its as easy as

addToLayout (component, anchorTopLeft, anchorBottomRight);

This example program shows how easy it is, check out the constructor for MainPanel

#include "juce.h"

//==============================================================================
/**
  Smoothly repositions and resizes child Components without rounding errors,
  according to caller provided stretching parameters.

  Usage:

  1) Derive your Component from ResizableLayout

  2) Give your Component a well defined default size in its constructor

  3) Add child Components and position them according to how
     they should appear at the default.

  4) For each child Component, also call addToLayout() and specify what
     portion of the owner's growth or reduction should apply to each corner.

  5) At the end of your constructor, call activateLayout() to turn it on

  6) If you later manually reposition a control (for example, using a
     ComponentDragger) call updateLayoutFor() on the control that moved,
     or if you moved all the controls call updateLayout().
*/
class ResizableLayout : private ComponentListener
{
public:
	enum
	{
		anchorUnit=100
	};

	enum Style
	{
		styleStretch,
		styleFixedAspect
	};

	static const Point<int> anchorNone;
	static const Point<int> anchorTopLeft;
	static const Point<int> anchorTopCenter;
	static const Point<int> anchorTopRight;
	static const Point<int> anchorMidLeft;
	static const Point<int> anchorMidCenter;
	static const Point<int> anchorMidRight;
	static const Point<int> anchorBottomLeft;
	static const Point<int> anchorBottomCenter;
	static const Point<int> anchorBottomRight;

public:
	ResizableLayout (Component* owner);
	~ResizableLayout ();

	// Add a Component to the Layout.
	// topLeft and bottomRight are the percentages that the top left and bottom right of
	// the Component should move by, when the layout is resized.
	// So if you wanted to have the control take on the full width of the parent, and
	// half the height, you would use bottomRight.x=100, bottomRight.y=50. or
  // use the constant anchorMidRight
	void addToLayout (
		Component *component,
		const Point<int> &topLeft,
		const Point<int> &bottomRight=anchorNone,
		Style style = styleStretch );

	// Remove a Component from the Layout.
	void removeFromLayout (Component* component);

	// Activate (or deactivate) the Layout. The Layout is initially inactive,
	// to prevent spurious recalculation while a Component and its children are being
	// constructed (causing resized() messages). Activating the Layout for the
	// first time will cause an Update().
	void activateLayout (bool bActive=true);

	// Update the state information for all items. This is used on the first Activate(),
	// and can also be used if multiple controls are moved or resized from elsewhere.
	void updateLayout ();

	// Call this to manually update the state information for a single control
	// after it has been moved or resized from elsewhere.
	void updateLayoutFor (Component *component);

private:
  struct Rect
  {
	  Rect() {}
	  Rect( int top0, int left0, int bottom0, int right0 ) { top=top0; left=left0; bottom=bottom0; right=right0; }
	  Rect( const Rectangle<int> &r ) { top=int(r.getY()); left=int(r.getX()); bottom=int(r.getBottom()); right=int(r.getRight()); }
	  operator Rectangle<int>() const { return Rectangle<int>( left, top, Width(), Height() ); }
	  int Height( void ) const { return bottom-top; }
	  int Width( void ) const { return right-left; }
	  void Inset( int dx, int dy ) { top+=dy; left+=dx; bottom-=dy; right-=dx; }

	  int top;
	  int left;
	  int bottom;
	  int right;
  };

  struct Anchor
	{
		Style	style;
		Component* component;
		Point<int> topLeft;
		Point<int> bottomRight;

    Anchor (Component* component=0);
    bool operator== (const Anchor& rhs) const;
    bool operator>= (const Anchor& rhs) const;
	};

  struct State
	{
		Component* component;
		double aspect;
		Rect margin;

    State (Component* component=0);
    bool operator== (const State& rhs) const;
    bool operator>= (const State& rhs) const;
	};

  void addStateFor (const Anchor& anchor);

	void recalculateLayout ();

  void componentMovedOrResized (Component& component,
                                bool wasMoved,
                                bool wasResized);

  void componentBeingDeleted (Component& component);

private:
	Component* m_owner;

  SortedSet<Anchor> m_anchors;
  SortedSet<State> m_states;

	bool m_bFirstTime;
	bool m_bActive;
};

const Point<int> ResizableLayout::anchorNone			  ( -1, -1 );
const Point<int> ResizableLayout::anchorTopLeft		  ( 0, 0 );
const Point<int> ResizableLayout::anchorTopCenter		( anchorUnit/2, 0 );
const Point<int> ResizableLayout::anchorTopRight		( anchorUnit, 0 );
const Point<int> ResizableLayout::anchorMidLeft		  ( 0, anchorUnit/2 );
const Point<int> ResizableLayout::anchorMidCenter		( anchorUnit/2, anchorUnit/2 );
const Point<int> ResizableLayout::anchorMidRight		( anchorUnit, anchorUnit/2 );
const Point<int> ResizableLayout::anchorBottomLeft	( 0, anchorUnit );
const Point<int> ResizableLayout::anchorBottomCenter( anchorUnit/2, anchorUnit );
const Point<int> ResizableLayout::anchorBottomRight	( anchorUnit, anchorUnit );

ResizableLayout::Anchor::Anchor (Component* component_)
: component (component_)
{
  jassert (component);
}

bool ResizableLayout::Anchor::operator== (const Anchor& rhs) const
  { return component == rhs.component; }

bool ResizableLayout::Anchor::operator>= (const Anchor& rhs) const
  { return component >= rhs.component; }

ResizableLayout::State::State (Component* component_)
: component (component_)
{
  jassert (component);
}

bool ResizableLayout::State::operator== (const State& rhs) const
  { return component == rhs.component; }

bool ResizableLayout::State::operator>= (const State& rhs) const
  { return component >= rhs.component; }

//----

ResizableLayout::ResizableLayout (Component* owner)
: m_owner (owner)
{
  m_bFirstTime = true;
	m_bActive = false;

  m_owner->addComponentListener (this);
}


ResizableLayout::~ResizableLayout()
{
}

void ResizableLayout::addToLayout (Component* component,
                                   const Point<int> &topLeft,
                                   const Point<int> &bottomRight,
                                   Style style )
{
	jassert (topLeft!=anchorNone);

  Anchor anchor (component);
	anchor.style = style;
  anchor.topLeft = topLeft;
	anchor.bottomRight = bottomRight;

  m_anchors.add (anchor);

  //component->addComponentListener (this);
}

void ResizableLayout::removeFromLayout (Component* component)
{
  m_anchors.removeValue (component);
  m_states.removeValue (component);
}

void ResizableLayout::activateLayout( bool bActive )
{
	if( m_bActive!=bActive )
	{
		if( bActive && m_bFirstTime )
		{
			updateLayout();
			m_bFirstTime=false;
		}

		m_bActive=bActive;
	}
}

void ResizableLayout::updateLayout ()
{
  m_states.clearQuick();
	for( int i=0; i<m_anchors.size(); i++ )
		addStateFor (m_anchors[i]);
}

void ResizableLayout::updateLayoutFor (Component *component)
{
  m_states.removeValue (component);
  addStateFor (m_anchors[m_anchors.indexOf (Anchor(component))]);
}

void ResizableLayout::addStateFor (const Anchor& anchor)
{
	Rect rBounds = anchor.component->getBounds();

  // owner size
	Point<int> ptSize( int(m_owner->getWidth()), int(m_owner->getHeight()) );

  State state (anchor.component);

  // secret sauce
	state.margin.top = rBounds.top - (ptSize.getY() * anchor.topLeft.getY()) / anchorUnit;
	state.margin.left = rBounds.left - (ptSize.getX() * anchor.topLeft.getX()) / anchorUnit;
	state.margin.bottom = rBounds.bottom - (ptSize.getY() * anchor.bottomRight.getY()) / anchorUnit;
	state.margin.right = rBounds.right - (ptSize.getX() * anchor.bottomRight.getX()) / anchorUnit;

  state.aspect = double (rBounds.Width()) / rBounds.Height();

  m_states.add (state);
}

// Recalculate the position and size of all the controls
// in the layout, based on the owner Component size.
void ResizableLayout::recalculateLayout()
{
	if( m_bActive )
	{
		Rect rParent = m_owner->getBounds();
		
    for( int i=0; i<m_states.size(); i++ )
		{
      Anchor anchor = m_anchors[i];
      State state = m_states[i];
      jassert (anchor.component == state.component);

      Rect rBounds;

      // secret sauce
			rBounds.top = state.margin.top + (rParent.Height() * anchor.topLeft.getY()) / anchorUnit;
			rBounds.left = state.margin.left + (rParent.Width() * anchor.topLeft.getX()) / anchorUnit;
			if( anchor.bottomRight != anchorNone )
			{
				rBounds.bottom= state.margin.bottom + (rParent.Height() * anchor.bottomRight.getY()) / anchorUnit;
				rBounds.right = state.margin.right + (rParent.Width() * anchor.bottomRight.getX()) / anchorUnit;
			}
			else
			{
				rBounds.bottom = rBounds.top + anchor.component->getHeight();
				rBounds.right = rBounds.left + anchor.component->getWidth();
			}

			if (anchor.style == styleStretch)
			{
				anchor.component->setBounds (rBounds);
			}
			else if (anchor.style==styleFixedAspect)
			{
				Rect rItem;
				double aspect = double (rBounds.Width()) / rBounds.Height();

				if( aspect > state.aspect )
				{
					rItem.top = rBounds.top;
					rItem.bottom = rBounds.bottom;
					int width = int (state.aspect * rItem.Height());
					rItem.left = rBounds.left + (rBounds.Width()-width)/2;
					rItem.right = rItem.left + width;
				}
				else
				{
					rItem.left = rBounds.left;
					rItem.right = rBounds.right;
					int height = int (1. / state.aspect * rItem.Width());
					rItem.top = rBounds.top + (rBounds.Height() - height) / 2;
					rItem.bottom = rItem.top + height;
				}

				anchor.component->setBounds( rItem );
			}
		}
	}
}

void ResizableLayout::componentMovedOrResized (Component& component,
                                               bool wasMoved,
                                               bool wasResized)
{
  if( &component == m_owner )
  {
    if (wasResized)
    {
      recalculateLayout ();
    }
  }
}

void ResizableLayout::componentBeingDeleted (Component& component)
{
  m_anchors.removeValue (&component);
  m_states.removeValue (&component);
}

struct MainPanel : Component, ResizableLayout
{
  MainPanel() : ResizableLayout (this)
  {
    // create the initial layout

    // must have a defined initial size
    setSize(512, 384);

    TextButton* b;

    b = new TextButton ("Juce");
    b->setBounds (32, 32, 512-64, 32);
    addToLayout (b, anchorTopLeft, anchorTopRight);
    addAndMakeVisible (b);

    b = new TextButton ("One");
    b->setBounds (32, 96, 149, 32);
    b->setConnectedEdges (Button::ConnectedOnRight );
    addToLayout (b, Point<int>(0,0), Point<int>(33,0));
    addAndMakeVisible (b);

    b = new TextButton ("Two");
    b->setBounds (32+149, 96, 150, 32);
    b->setConnectedEdges (Button::ConnectedOnLeft | Button::ConnectedOnRight );
    addToLayout (b, Point<int>(33,0), Point<int>(66,0));
    addAndMakeVisible (b);

    b = new TextButton ("Three");
    b->setBounds (32+300, 96, 149, 32);
    b->setConnectedEdges (Button::ConnectedOnLeft);
    addToLayout (b, Point<int>(66,0), Point<int>(100,0));
    addAndMakeVisible (b);

    b = new TextButton ("Side");
    b->setBounds (512-160, 160, 128, 128);
    addToLayout (b, anchorTopRight, anchorBottomRight);
    addAndMakeVisible (b);

    b = new TextButton ("BR");
    b->setBounds (512-160, 384-64, 128, 32);
    addToLayout (b, anchorBottomRight);
    addAndMakeVisible (b);

    b = new TextButton ("Stretchy");
    b->setBounds (32+192+32, 128+32, 64, 192);
    b->setConnectedEdges (Button::ConnectedOnTop | Button::ConnectedOnLeft | Button::ConnectedOnBottom | Button::ConnectedOnRight );
    addToLayout (b, anchorTopCenter, anchorBottomRight);
    addAndMakeVisible (b);

    b = new TextButton ("Aspect");
    b->setBounds (32, 128+32, 192, 192);
    b->setConnectedEdges (Button::ConnectedOnTop | Button::ConnectedOnLeft | Button::ConnectedOnBottom | Button::ConnectedOnRight );
    addToLayout (b, anchorTopLeft, anchorBottomCenter, styleFixedAspect);
    addAndMakeVisible (b);

    // turn it on
    activateLayout ();
  }
  ~MainPanel() { deleteAllChildren(); }
  void paint (Graphics& g)
  {
    g.setColour (Colours::grey);
    g.fillAll();
  }
};

struct MainWindow : DocumentWindow
{
  MainWindow()
  : DocumentWindow (JUCE_T("Test")
  , Colours::black
  , DocumentWindow::allButtons
  , true )
  {
    MainPanel* p = new MainPanel;
    setResizable (true, false);
    setContentComponent (p, true, true);
    centreWithSize (getWidth(), getHeight());
    setVisible( true );
  }
  ~MainWindow() {}

  void closeButtonPressed() { JUCEApplication::quit(); }
};

struct MainApp : JUCEApplication
{
  MainApp() : mainWindow(0) { s_app=this; }
  ~MainApp() { s_app=0; }
  static MainApp& GetInstance() { return *s_app; }
  const String getApplicationName() { return JUCE_T("JuceTest"); }
  const String getApplicationVersion() { return JUCE_T("0.1.0"); }
  bool moreThanOneInstanceAllowed() { return true; }
  void anotherInstanceStarted (const String& commandLine) {}

  void initialise (const String& commandLine)
  {
    mainWindow = new MainWindow;
  }

  void shutdown()
  {
    delete mainWindow;
  }

  static MainApp* s_app;
  MainWindow* mainWindow;
};

MainApp* MainApp::s_app = 0;

START_JUCE_APPLICATION (MainApp)

I would love to hear any complaints, criticisms, praise, advice, suggestions, or requests for improvements. I know the code is a little bit messy especially my weirdo “Rect” class (which helped me port it from my previous homebrew framework). If you use this class, please tell me about your experiences!


#2

Thanks for sharing.


#3

Did it work for you?


#4

It seems very useful. I’ll used this class sooner or later.


#5

These layout classes are now part of VFLib: