Files
2026-08-16 18:24:52 +07:00

915 lines
23 KiB
C++

// This file is part of VSTGUI. It is subject to the license terms
// in the LICENSE file found in the top-level directory of this
// distribution and at http://github.com/steinbergmedia/vstgui/LICENSE
#include "csplitview.h"
#include "cframe.h"
#include "cdrawcontext.h"
#include "events.h"
#include "viewlayouter/baseviewlayouter.h"
#include "../uidescription/icontroller.h"
#include <list>
namespace VSTGUI {
/// @cond ignore
//-----------------------------------------------------------------------------
class CSplitViewSeparatorView : public CViewContainer
{
public:
CSplitViewSeparatorView (const CRect& size, CSplitView::Style style, int32_t index);
void drawRect (CDrawContext *pContext, const CRect& r) override;
void onMouseDownEvent (MouseDownEvent& event) override;
void onMouseUpEvent (MouseUpEvent& event) override;
void onMouseMoveEvent (MouseMoveEvent& event) override;
void mouseMoved (MouseEvent& event);
void onMouseEnterEvent (MouseEnterEvent& event) override;
void onMouseExitEvent (MouseExitEvent& event) override;
bool hitTestSubViews (const CPoint& where, const Event& event) override;
bool removed (CView* parent) override;
protected:
CPoint lastMousePos;
CRect startSize;
CSplitView::Style style;
int32_t index;
int32_t flags;
};
//-----------------------------------------------------------------------------
static ISplitViewController* getSplitViewController (const CView* view)
{
IController* controller = getViewController (view, true);
if (controller)
{
return dynamic_cast<ISplitViewController*> (controller);
}
return nullptr;
}
//-----------------------------------------------------------------------------
struct CSplitView::SplitViewLayouter final : BaseViewLayouter,
NonAtomicReferenceCounted
{
LayoutData resizeFirstView (const CSplitView& splitView, const Children& children,
const CRect& newSize, CPoint diff)
{
LayoutData layoutData;
layoutData.reserve (children.size ());
auto it = children.begin ();
if (it != children.end ())
{
CView* view = *it;
auto r = view->getViewSize ();
r.right += diff.x;
r.bottom += diff.y;
std::optional<ViewLayout> childLayout;
if (auto childContainer = view->asViewContainer ())
childLayout = childContainer->calculateViewLayout (r);
layoutData.push_back ({view->getRuntimeID (), r, r, std::move (childLayout)});
++it;
}
for (; it != children.end (); ++it)
{
CView* view = *it;
auto r = view->getViewSize ();
if (splitView.getStyle () == CSplitView::kHorizontal)
{
r.offset (diff.x, 0);
r.bottom += diff.y;
}
else
{
r.offset (0, diff.y);
r.right += diff.x;
}
layoutData.push_back ({view->getRuntimeID (), r, r});
}
return layoutData;
}
LayoutData resizeSecondView (const CSplitView& splitView, const Children& children,
const CRect& newSize, CPoint diff)
{
LayoutData layoutData;
layoutData.reserve (children.size ());
int32_t viewIndex = 0;
for (auto it = children.begin (); it != children.end (); ++it)
{
CView* view = *it;
auto* separatorView = dynamic_cast<CSplitViewSeparatorView*> (view);
if (!separatorView)
viewIndex++;
auto r = view->getViewSize ();
if (separatorView == nullptr && viewIndex == 2)
{
r.right += diff.x;
r.bottom += diff.y;
}
else if (viewIndex == 1)
{
if (splitView.getStyle () == CSplitView::kHorizontal)
{
r.bottom += diff.y;
}
else
{
r.right += diff.x;
}
}
else if (viewIndex > 1)
{
if (splitView.getStyle () == CSplitView::kHorizontal)
{
r.offset (diff.x, 0);
r.bottom += diff.y;
}
else
{
r.offset (0, diff.y);
r.right += diff.x;
}
}
layoutData.push_back ({view->getRuntimeID (), r, r});
}
return layoutData;
}
LayoutData resizeLastView (const CSplitView& splitView, const Children& children,
const CRect& newSize, CPoint diff)
{
LayoutData layoutData;
layoutData.reserve (children.size ());
auto it = children.rbegin ();
if (it != children.rend ())
{
CView* view = *it;
auto r = view->getViewSize ();
r.right += diff.x;
r.bottom += diff.y;
std::optional<ViewLayout> childLayout;
if (auto childContainer = view->asViewContainer ())
childLayout = childContainer->calculateViewLayout (r);
layoutData.push_back ({view->getRuntimeID (), r, r, std::move (childLayout)});
++it;
}
for (; it != children.rend (); ++it)
{
CView* view = *it;
auto r = view->getViewSize ();
if (splitView.getStyle () == CSplitView::kHorizontal)
{
r.bottom += diff.y;
}
else
{
r.right += diff.x;
}
layoutData.push_back ({view->getRuntimeID (), r, r});
}
std::reverse (layoutData.begin (), layoutData.end ());
return layoutData;
}
LayoutData resizeViewsEqual (const CSplitView& splitView, const Children& children,
const CRect& newSize, CPoint diff)
{
LayoutData layoutData;
if (children.empty ())
return layoutData;
auto numViews = children.size ();
layoutData.reserve (numViews);
auto numSeparators = numViews / 2u;
numViews -= numSeparators;
CPoint offset;
if (splitView.getStyle () == CSplitView::kHorizontal)
{
diff.x /= numViews;
offset.y = diff.y;
}
else
{
diff.y /= numViews;
offset.x = diff.x;
}
for (const auto& view : children)
{
if (auto* separatorView = dynamic_cast<CSplitViewSeparatorView*> (view.get ()))
{
auto r = separatorView->getViewSize ();
if (splitView.getStyle () == CSplitView::kHorizontal)
{
r.offset (offset.x, 0);
r.bottom += offset.y;
}
else
{
r.offset (0, offset.y);
r.right += offset.x;
}
layoutData.push_back ({view->getRuntimeID (), r, r});
}
else
{
auto r = view->getViewSize ();
if (splitView.getStyle () == CSplitView::kHorizontal)
{
r.offset (offset.x, 0);
r.right += diff.x;
r.bottom += diff.y;
offset.x += diff.x;
}
else
{
r.offset (0, offset.y);
r.right += diff.x;
r.bottom += diff.y;
offset.y += diff.y;
}
layoutData.push_back ({view->getRuntimeID (), r, r});
}
}
return layoutData;
}
bool validateLayoutData (const CSplitView& splitView, LayoutData& result,
const CRect& newSize) const
{
bool isHorizontal = splitView.getStyle () == kHorizontal;
auto controller = getSplitViewController (&splitView);
auto sepIndex = 0;
auto view1 = result.begin ();
auto prevSep = result.end ();
for (; view1 != result.end (); ++sepIndex)
{
auto constraintSize = newSize;
constraintSize.originize ();
auto sep = std::next (view1);
auto view2 = std::next (sep);
auto nextSep = std::next (view2);
if (prevSep != result.end ())
{
if (isHorizontal)
constraintSize.left = prevSep->mouseSize.right;
else
constraintSize.top = prevSep->mouseSize.bottom;
}
if (nextSep != result.end ())
{
if (isHorizontal)
constraintSize.right = nextSep->mouseSize.left;
else
constraintSize.bottom = nextSep->mouseSize.top;
}
CCoord view1MinWidth = -1.;
CCoord view1MaxWidth = -1.;
CCoord view2MinWidth = -1.;
CCoord view2MaxWidth = -1.;
if (controller)
{
if (controller->getSplitViewSizeConstraint (sepIndex, view1MinWidth, view1MaxWidth,
const_cast<CSplitView*> (&splitView)) &&
view1MinWidth >= 0.)
{
if (isHorizontal)
constraintSize.left += view1MinWidth;
else
constraintSize.top += view1MinWidth;
}
if (controller->getSplitViewSizeConstraint (sepIndex + 1, view2MinWidth,
view2MaxWidth,
const_cast<CSplitView*> (&splitView)) &&
view2MinWidth >= 0.)
{
if (isHorizontal)
constraintSize.right -= view2MinWidth;
else
constraintSize.bottom -= view2MinWidth;
}
}
auto seperatorSize = sep->mouseSize;
if (isHorizontal)
{
if (seperatorSize.left < constraintSize.left)
seperatorSize.offset (constraintSize.left - seperatorSize.left, 0);
if (seperatorSize.right > constraintSize.right)
seperatorSize.offset (constraintSize.right - seperatorSize.right, 0);
}
else
{
if (seperatorSize.top < constraintSize.top)
seperatorSize.offset (0, constraintSize.top - seperatorSize.top);
if (seperatorSize.bottom > constraintSize.bottom)
seperatorSize.offset (0, constraintSize.bottom - seperatorSize.bottom);
}
CRect r1 (view1->viewSize);
CRect r2 (view2->viewSize);
if (isHorizontal)
{
r1.right = seperatorSize.left;
r2.left = seperatorSize.right;
}
else
{
r1.bottom = seperatorSize.top;
r2.top = seperatorSize.bottom;
}
// TODO: if one of the view is too small or too wide, we could check to move another
// separator together with this one
if (view1MaxWidth >= 0.)
{
if (isHorizontal)
{
if (r1.getWidth () > view1MaxWidth)
{
return false;
}
}
else
{
if (r1.getHeight () > view1MaxWidth)
{
return false;
}
}
}
if (view1MinWidth >= 0.)
{
if (isHorizontal)
{
if (r1.getWidth () < view1MinWidth)
{
return false;
}
}
else
{
if (r1.getHeight () < view1MinWidth)
{
return false;
}
}
}
if (view2MaxWidth >= 0.)
{
if (isHorizontal)
{
if (r2.getWidth () > view2MaxWidth)
{
return false;
}
}
else
{
if (r2.getHeight () > view2MaxWidth)
{
return false;
}
}
}
if (view2MinWidth >= 0.)
{
if (isHorizontal)
{
if (r2.getWidth () < view2MinWidth)
{
return false;
}
}
else
{
if (r2.getHeight () < view2MinWidth)
{
return false;
}
}
}
view1->mouseSize = r1;
view1->viewSize = r1;
view2->mouseSize = r2;
view2->viewSize = r2;
auto calculateChildLayout = [&] (auto&& it) {
if (auto child = splitView.findFirstViewIf (
[&] (auto&& view) { return view->getRuntimeID () == it->viewId; }))
{
if (auto containerChild = child->asViewContainer ())
{
it->childLayout = containerChild->calculateViewLayout (it->viewSize);
}
}
};
calculateChildLayout (view1);
calculateChildLayout (view2);
sep->viewSize = seperatorSize;
sep->mouseSize = seperatorSize;
if (nextSep == result.end ())
break;
view1 = view2;
prevSep = sep;
}
return true;
}
std::optional<ViewLayout> calculateLayout (const CViewContainer& view, const Children& children,
const CRect& newSize) override
{
if (children.size () < 3u)
return {{newSize, LayoutData {}}};
auto& splitView = static_cast<const CSplitView&> (view);
auto oldSize = view.getViewSize ();
CPoint diff (newSize.getWidth () - oldSize.getWidth (),
newSize.getHeight () - oldSize.getHeight ());
if (diff.x == 0. && diff.y == 0.)
return {{newSize, LayoutData {}}};
LayoutData result;
switch (splitView.getResizeMethod ())
{
case CSplitView::kResizeFirstView:
{
result = resizeFirstView (splitView, children, newSize, diff);
break;
}
case CSplitView::kResizeSecondView:
{
result = resizeSecondView (splitView, children, newSize, diff);
break;
}
case CSplitView::kResizeLastView:
{
result = resizeLastView (splitView, children, newSize, diff);
break;
}
case CSplitView::kResizeAllViews:
{
result = resizeViewsEqual (splitView, children, newSize, diff);
break;
}
}
validateLayoutData (splitView, result, newSize);
return {{newSize, std::move (result)}};
}
};
/// @endcond
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
CSplitView::CSplitView (const CRect& size, Style style, CCoord separatorWidth,
ISplitViewSeparatorDrawer* separatorDrawer)
: CViewContainer (size)
, style (style)
, resizeMethod (kResizeLastView)
, separatorWidth (separatorWidth)
, separatorDrawer (separatorDrawer)
{
setViewLayouter (makeOwned<SplitViewLayouter> ());
}
//-----------------------------------------------------------------------------
void CSplitView::setStyle (Style s)
{
if (style != s)
{
style = s;
// TODO: rearrange views
}
}
//-----------------------------------------------------------------------------
void CSplitView::setResizeMethod (ResizeMethod method) { resizeMethod = method; }
//-----------------------------------------------------------------------------
void CSplitView::setSeparatorWidth (CCoord width)
{
if (width != separatorWidth)
{
SplitViewLayouter::LayoutData layoutData;
for (auto& child : getChildren ())
{
auto viewSize = child->getViewSize ();
auto mouseSize = child->getMouseableArea ();
if (child.cast<CSplitViewSeparatorView> ())
{
if (style == kHorizontal)
{
viewSize.setWidth (width);
mouseSize.setWidth (width);
}
else
{
viewSize.setHeight (width);
mouseSize.setHeight (width);
}
}
layoutData.push_back ({child->getRuntimeID (), viewSize, mouseSize, {}});
}
if (auto layouter = getViewLayouter ().cast<SplitViewLayouter> ())
{
if (layouter->validateLayoutData (*this, layoutData, getViewSize ()))
{
applyViewLayout ({getViewSize (), layoutData});
}
}
separatorWidth = width;
}
}
//-----------------------------------------------------------------------------
void CSplitView::setViewSize (const CRect& rect, bool invalid)
{
CViewContainer::setViewSize (rect, invalid);
}
//-----------------------------------------------------------------------------
bool CSplitView::addView (CView* pView, CView* pBefore)
{
vstgui_assert (pBefore == nullptr);
CRect viewSize (pView->getViewSize ());
viewSize.originize ();
if (style == kHorizontal)
viewSize.setHeight (getHeight ());
else
viewSize.setWidth (getWidth ());
ReverseViewIterator it (this);
if (*it)
{
CView* lastView = *it;
CRect r (lastView->getViewSize ());
if (style == kHorizontal)
{
r.left = r.right;
r.right += getSeparatorWidth ();
viewSize.offset (r.right, 0);
}
else
{
r.top = r.bottom;
r.bottom += getSeparatorWidth ();
viewSize.offset (0, r.bottom);
}
auto* separator = new CSplitViewSeparatorView (r, getStyle (), (getNbViews () - 1) / 2);
CViewContainer::addView (separator, nullptr);
}
pView->setViewSize (viewSize);
pView->setMouseableArea (viewSize);
return CViewContainer::addView (pView, nullptr);
}
//-----------------------------------------------------------------------------
bool CSplitView::removeView (CView* pView, bool withForget)
{
ReverseViewIterator it (this);
while (*it)
{
if (*it == pView)
{
++it;
if (*it)
{
if (auto* sepView = dynamic_cast<CSplitViewSeparatorView*> (*it))
{
CViewContainer::removeView (sepView, true);
}
}
else
{
if (auto* sepView = dynamic_cast<CSplitViewSeparatorView*> (getView (1)))
{
CViewContainer::removeView (sepView, true);
}
}
break;
}
++it;
}
return CViewContainer::removeView (pView, withForget);
}
//-----------------------------------------------------------------------------
bool CSplitView::removeAll (bool withForget)
{
return CViewContainer::removeAll (withForget);
}
//-----------------------------------------------------------------------------
bool CSplitView::sizeToFit ()
{
return false;
}
//-----------------------------------------------------------------------------
void CSplitView::storeViewSizes ()
{
ISplitViewController* controller = getSplitViewController (this);
if (controller)
{
int32_t index = 0;
ViewIterator it (this);
while (*it)
{
auto* sepView = dynamic_cast<CSplitViewSeparatorView*> (*it);
if (sepView == nullptr)
{
CRect r ((*it)->getViewSize ());
controller->storeViewSize (index, style == kHorizontal ? r.getWidth () : r.getHeight (), this);
index++;
}
++it;
}
}
}
//-----------------------------------------------------------------------------
bool CSplitView::removed (CView* parent)
{
storeViewSizes ();
return CViewContainer::removed (parent);
}
//-----------------------------------------------------------------------------
bool CSplitView::attached (CView* parent)
{
bool result = CViewContainer::attached (parent);
ISplitViewController* controller = getSplitViewController (this);
if (controller)
{
CRect r;
CPoint offset;
int32_t index = 0;
ViewIterator it (this);
while (*it)
{
if (auto* sepView = dynamic_cast<CSplitViewSeparatorView*> (*it))
{
r = sepView->getViewSize ();
r.offset (offset.x, offset.y);
sepView->setViewSize (r);
sepView->setMouseableArea (r);
}
else
{
CView* view = *it;
r = view->getViewSize ();
r.offset (offset.x, offset.y);
CCoord size;
if (controller->restoreViewSize (index, size, this))
{
if (style == kHorizontal)
{
offset.x += size - r.getWidth ();
r.setWidth (size);
}
else
{
offset.y += size - r.getHeight ();
r.setHeight (size);
}
}
view->setViewSize (r);
view->setMouseableArea (r);
index++;
}
++it;
}
}
return result;
}
//-----------------------------------------------------------------------------
bool CSplitView::requestNewSeparatorSize (CSplitViewSeparatorView* separatorView, CRect newSize)
{
if (inApplyViewLayout ())
return false;
ViewIterator it (this);
CView* view1 = nullptr;
CView* view2 = nullptr;
while (*it)
{
if (*it == separatorView)
{
++it;
view2 = *it;
break;
}
else
{
view1 = *it;
}
++it;
}
if (view1 && view2)
{
auto size = view1->getViewSize ();
size.unite (view2->getViewSize ());
SplitViewLayouter::LayoutData layoutData;
layoutData.push_back (
{view1->getRuntimeID (), view1->getViewSize (), view1->getMouseableArea (), {}});
layoutData.push_back ({separatorView->getRuntimeID (), newSize, newSize, {}});
layoutData.push_back (
{view2->getRuntimeID (), view2->getViewSize (), view2->getMouseableArea (), {}});
if (auto layouter = getViewLayouter ().cast<SplitViewLayouter> ())
{
if (layouter->validateLayoutData (*this, layoutData, size))
{
return applyViewLayout ({getViewSize (), layoutData});
}
}
}
return false;
}
//-----------------------------------------------------------------------------
ISplitViewSeparatorDrawer* CSplitView::getDrawer ()
{
ISplitViewSeparatorDrawer* drawer = nullptr;
ISplitViewController* controller = getSplitViewController (this);
if (controller)
drawer = controller->getSplitViewSeparatorDrawer (this);
return drawer ? drawer : separatorDrawer;
}
//-----------------------------------------------------------------------------
bool CSplitView::addViewToSeparator (int32_t sepIndex, CView* view)
{
ViewIterator it (this);
while (*it)
{
if (auto* sepView = dynamic_cast<CSplitViewSeparatorView*>(*it))
{
if (sepIndex == 0)
{
sepView->addView (view);
return true;
}
sepIndex--;
}
++it;
}
return false;
}
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
CSplitViewSeparatorView::CSplitViewSeparatorView (const CRect& size, CSplitView::Style style, int32_t index)
: CViewContainer (size)
, style (style)
, index (index)
, flags (0)
{
setTransparency (true);
}
//-----------------------------------------------------------------------------
void CSplitViewSeparatorView::drawRect (CDrawContext *pContext, const CRect& r)
{
auto* splitView = static_cast<CSplitView*> (getParentView ());
ISplitViewSeparatorDrawer* drawer = splitView ? splitView->getDrawer () : nullptr;
if (drawer)
{
drawer->drawSplitViewSeparator (pContext, getViewSize (), flags, index, splitView);
}
CViewContainer::drawRect (pContext, r);
}
//------------------------------------------------------------------------
bool CSplitViewSeparatorView::hitTestSubViews (const CPoint& where, const Event& event)
{
return hitTest (where, event);
}
//-----------------------------------------------------------------------------
void CSplitViewSeparatorView::onMouseDownEvent (MouseDownEvent& event)
{
if (CViewContainer::hitTestSubViews (event.mousePosition, event))
{
CViewContainer::onMouseDownEvent (event);
}
else if (event.buttonState.isLeft ())
{
setBit (flags, ISplitViewSeparatorDrawer::kMouseDown, true);
lastMousePos = event.mousePosition;
startSize = getViewSize ();
invalid ();
mouseMoved (event);
}
}
//-----------------------------------------------------------------------------
void CSplitViewSeparatorView::onMouseUpEvent (MouseUpEvent& event)
{
if (getMouseDownView ())
CViewContainer::onMouseUpEvent (event);
else if (hasBit (flags, ISplitViewSeparatorDrawer::kMouseDown))
{
setBit (flags, ISplitViewSeparatorDrawer::kMouseDown, false);
invalid ();
event.consumed = true;
}
}
//-----------------------------------------------------------------------------
void CSplitViewSeparatorView::onMouseMoveEvent (MouseMoveEvent& event)
{
if (getMouseDownView ())
CViewContainer::onMouseMoveEvent (event);
else
mouseMoved (event);
}
//-----------------------------------------------------------------------------
void CSplitViewSeparatorView::mouseMoved (MouseEvent& event)
{
if (hasBit (flags, ISplitViewSeparatorDrawer::kMouseDown))
{
if (event.mousePosition != lastMousePos)
{
CRect newSize (startSize);
if (style == CSplitView::kHorizontal)
newSize.offset (event.mousePosition.x - lastMousePos.x, 0);
else
newSize.offset (0, event.mousePosition.y - lastMousePos.y);
auto* splitView = static_cast<CSplitView*> (getParentView ());
if (splitView)
splitView->requestNewSeparatorSize (this, newSize);
}
event.consumed = true;
}
else if (!hasBit (flags, ISplitViewSeparatorDrawer::kMouseOver))
{
if (!CViewContainer::hitTestSubViews (event.mousePosition, event) && hitTest (event.mousePosition, event))
{
MouseEnterEvent enterEvent (event.mousePosition, event.buttonState, event.modifiers);
onMouseEnterEvent (enterEvent);
if (enterEvent.consumed)
event.consumed = true;
}
}
else if (hasBit (flags, ISplitViewSeparatorDrawer::kMouseOver))
{
if (CViewContainer::hitTestSubViews (event.mousePosition, event))
{
MouseExitEvent exitEvent (event.mousePosition, event.buttonState, event.modifiers);
onMouseExitEvent (exitEvent);
if (exitEvent.consumed)
event.consumed = true;
}
}
}
//-----------------------------------------------------------------------------
void CSplitViewSeparatorView::onMouseEnterEvent (MouseEnterEvent& event)
{
if (CViewContainer::hitTestSubViews (event.mousePosition, event))
{
event.consumed = true;
return;
}
setBit (flags, ISplitViewSeparatorDrawer::kMouseOver, true);
invalid ();
if (style == CSplitView::kHorizontal)
getFrame ()->setCursor (kCursorHSize);
else
getFrame ()->setCursor (kCursorVSize);
event.consumed = true;
}
//-----------------------------------------------------------------------------
void CSplitViewSeparatorView::onMouseExitEvent (MouseExitEvent& event)
{
setBit (flags, ISplitViewSeparatorDrawer::kMouseOver, false);
invalid ();
getFrame ()->setCursor (kCursorDefault);
event.consumed = true;
}
//-----------------------------------------------------------------------------
bool CSplitViewSeparatorView::removed (CView* parent)
{
if (hasBit (flags, ISplitViewSeparatorDrawer::kMouseOver) && getFrame ())
getFrame ()->setCursor (kCursorDefault);
return CViewContainer::removed (parent);
}
} // VSTGUI