Files
XCEngine/engine/include/XCEngine/UI/Layout/UISplitterLayout.h

312 lines
11 KiB
C++

#pragma once
#include <XCEngine/UI/Layout/LayoutTypes.h>
#include <algorithm>
#include <cmath>
namespace XCEngine {
namespace UI {
namespace Layout {
struct UISplitterMetrics {
float thickness = 8.0f;
float hitThickness = 12.0f;
};
struct UISplitterConstraints {
float primaryMin = 0.0f;
float primaryMax = GetUnboundedLayoutExtent();
float secondaryMin = 0.0f;
float secondaryMax = GetUnboundedLayoutExtent();
};
struct UISplitterLayoutOptions {
UILayoutAxis axis = UILayoutAxis::Horizontal;
float ratio = 0.5f;
float handleThickness = 8.0f;
float minPrimaryExtent = 0.0f;
float minSecondaryExtent = 0.0f;
};
struct UISplitterLayoutResult {
UIRect primaryRect = {};
UIRect handleRect = {};
UIRect secondaryRect = {};
float resolvedRatio = 0.5f;
float splitRatio = 0.5f;
float primaryExtent = 0.0f;
float secondaryExtent = 0.0f;
};
namespace SplitterDetail {
inline float ClampSplitterExtent(float value) {
return (std::max)(0.0f, value);
}
inline float ClampFiniteExtent(float value, float minValue, float maxValue) {
return (std::clamp)(value, minValue, maxValue);
}
inline float ResolveOverflowPrimaryExtentFromMinimums(
float usableExtent,
float preferredPrimaryMinimum,
float preferredSecondaryMinimum) {
const float clampedUsableExtent = ClampSplitterExtent(usableExtent);
const float primaryMinimum = ClampSplitterExtent(preferredPrimaryMinimum);
const float secondaryMinimum = ClampSplitterExtent(preferredSecondaryMinimum);
const float totalMinimum = primaryMinimum + secondaryMinimum;
if (clampedUsableExtent <= 0.0f) {
return 0.0f;
}
if (totalMinimum <= 0.0f) {
return clampedUsableExtent * 0.5f;
}
return clampedUsableExtent * (primaryMinimum / totalMinimum);
}
inline float GetMainExtent(const UISize& size, UILayoutAxis axis) {
return axis == UILayoutAxis::Horizontal ? size.width : size.height;
}
inline float GetCrossExtent(const UISize& size, UILayoutAxis axis) {
return axis == UILayoutAxis::Horizontal ? size.height : size.width;
}
inline float GetMainExtent(const UIRect& rect, UILayoutAxis axis) {
return axis == UILayoutAxis::Horizontal ? rect.width : rect.height;
}
inline float GetCrossExtent(const UIRect& rect, UILayoutAxis axis) {
return axis == UILayoutAxis::Horizontal ? rect.height : rect.width;
}
} // namespace SplitterDetail
inline UISize MeasureSplitterDesiredSize(
UILayoutAxis axis,
const UISize& primarySize,
const UISize& secondarySize,
float handleThickness) {
const float clampedHandleThickness = SplitterDetail::ClampSplitterExtent(handleThickness);
if (axis == UILayoutAxis::Horizontal) {
return UISize(
primarySize.width + clampedHandleThickness + secondarySize.width,
(std::max)(primarySize.height, secondarySize.height));
}
return UISize(
(std::max)(primarySize.width, secondarySize.width),
primarySize.height + clampedHandleThickness + secondarySize.height);
}
inline float ClampSplitterRatio(
const UISplitterLayoutOptions& options,
float totalMainExtent) {
const float mainExtent = SplitterDetail::ClampSplitterExtent(totalMainExtent);
const float handleThickness = (std::min)(
SplitterDetail::ClampSplitterExtent(options.handleThickness),
mainExtent);
const float usableExtent = (std::max)(0.0f, mainExtent - handleThickness);
if (usableExtent <= 0.0f) {
return 0.5f;
}
const float requestedPrimaryExtent =
usableExtent * (std::clamp)(options.ratio, 0.0f, 1.0f);
const float minPrimaryExtent = (std::min)(
SplitterDetail::ClampSplitterExtent(options.minPrimaryExtent),
usableExtent);
const float minSecondaryExtent = (std::min)(
SplitterDetail::ClampSplitterExtent(options.minSecondaryExtent),
usableExtent);
float minimumPrimaryExtent = minPrimaryExtent;
float maximumPrimaryExtent = usableExtent - minSecondaryExtent;
if (minimumPrimaryExtent > maximumPrimaryExtent) {
const float overflowPrimaryExtent =
SplitterDetail::ResolveOverflowPrimaryExtentFromMinimums(
usableExtent,
options.minPrimaryExtent,
options.minSecondaryExtent);
return usableExtent <= 0.0f ? 0.5f : overflowPrimaryExtent / usableExtent;
}
const float clampedPrimaryExtent = (std::clamp)(
requestedPrimaryExtent,
minimumPrimaryExtent,
maximumPrimaryExtent);
return clampedPrimaryExtent / usableExtent;
}
inline float ClampSplitterRatio(
UILayoutAxis axis,
float requestedRatio,
float totalMainExtent,
const UISplitterConstraints& constraints,
const UISplitterMetrics& metrics) {
UISplitterLayoutOptions options = {};
options.axis = axis;
options.ratio = requestedRatio;
options.handleThickness = metrics.thickness;
options.minPrimaryExtent = constraints.primaryMin;
options.minSecondaryExtent = constraints.secondaryMin;
const float mainExtent = SplitterDetail::ClampSplitterExtent(totalMainExtent);
const float handleThickness = (std::min)(
SplitterDetail::ClampSplitterExtent(metrics.thickness),
mainExtent);
const float usableExtent = (std::max)(0.0f, mainExtent - handleThickness);
if (usableExtent <= 0.0f) {
return 0.5f;
}
const float requestedPrimaryExtent =
usableExtent * (std::clamp)(requestedRatio, 0.0f, 1.0f);
float minimumPrimaryExtent = (std::min)(
SplitterDetail::ClampSplitterExtent(constraints.primaryMin),
usableExtent);
float maximumPrimaryExtent = std::isfinite(constraints.primaryMax)
? (std::clamp)(constraints.primaryMax, minimumPrimaryExtent, usableExtent)
: usableExtent;
const float minimumFromSecondary = (std::max)(
0.0f,
usableExtent - (std::isfinite(constraints.secondaryMax)
? (std::max)(SplitterDetail::ClampSplitterExtent(constraints.secondaryMax), SplitterDetail::ClampSplitterExtent(constraints.secondaryMin))
: usableExtent));
const float maximumFromSecondary = (std::max)(
0.0f,
usableExtent - (std::min)(SplitterDetail::ClampSplitterExtent(constraints.secondaryMin), usableExtent));
minimumPrimaryExtent = (std::max)(minimumPrimaryExtent, minimumFromSecondary);
maximumPrimaryExtent = (std::min)(maximumPrimaryExtent, maximumFromSecondary);
const bool minimumsOverflow =
SplitterDetail::ClampSplitterExtent(constraints.primaryMin) +
SplitterDetail::ClampSplitterExtent(constraints.secondaryMin) >
usableExtent;
if (minimumPrimaryExtent > maximumPrimaryExtent) {
if (minimumsOverflow) {
const float overflowPrimaryExtent =
SplitterDetail::ResolveOverflowPrimaryExtentFromMinimums(
usableExtent,
constraints.primaryMin,
constraints.secondaryMin);
return usableExtent <= 0.0f ? 0.5f : overflowPrimaryExtent / usableExtent;
}
minimumPrimaryExtent = 0.0f;
maximumPrimaryExtent = usableExtent;
}
const float clampedPrimaryExtent = (std::clamp)(
requestedPrimaryExtent,
minimumPrimaryExtent,
maximumPrimaryExtent);
return clampedPrimaryExtent / usableExtent;
}
inline UISplitterLayoutResult ArrangeSplitterLayout(
const UISplitterLayoutOptions& options,
const UIRect& bounds) {
UISplitterLayoutResult result = {};
const float mainExtent = SplitterDetail::GetMainExtent(bounds, options.axis);
const float crossExtent = SplitterDetail::GetCrossExtent(bounds, options.axis);
const float handleThickness = (std::min)(
SplitterDetail::ClampSplitterExtent(options.handleThickness),
mainExtent);
const float usableExtent = (std::max)(0.0f, mainExtent - handleThickness);
result.resolvedRatio = ClampSplitterRatio(options, mainExtent);
result.primaryExtent = usableExtent * result.resolvedRatio;
result.secondaryExtent = (std::max)(0.0f, usableExtent - result.primaryExtent);
if (options.axis == UILayoutAxis::Horizontal) {
result.primaryRect = UIRect(
bounds.x,
bounds.y,
result.primaryExtent,
crossExtent);
result.handleRect = UIRect(
bounds.x + result.primaryExtent,
bounds.y,
handleThickness,
crossExtent);
result.secondaryRect = UIRect(
result.handleRect.x + handleThickness,
bounds.y,
result.secondaryExtent,
crossExtent);
} else {
result.primaryRect = UIRect(
bounds.x,
bounds.y,
crossExtent,
result.primaryExtent);
result.handleRect = UIRect(
bounds.x,
bounds.y + result.primaryExtent,
crossExtent,
handleThickness);
result.secondaryRect = UIRect(
bounds.x,
result.handleRect.y + handleThickness,
crossExtent,
result.secondaryExtent);
}
result.splitRatio = result.resolvedRatio;
return result;
}
inline float ResolveSplitterRatioFromPointerPosition(
const UISplitterLayoutOptions& options,
const UIRect& bounds,
float pointerMainPosition) {
const float mainExtent = SplitterDetail::GetMainExtent(bounds, options.axis);
const float handleThickness = (std::min)(
SplitterDetail::ClampSplitterExtent(options.handleThickness),
mainExtent);
const float usableExtent = (std::max)(0.0f, mainExtent - handleThickness);
if (usableExtent <= 0.0f) {
return 0.5f;
}
const float origin = options.axis == UILayoutAxis::Horizontal ? bounds.x : bounds.y;
UISplitterLayoutOptions pointerOptions = options;
pointerOptions.ratio = (pointerMainPosition - origin - handleThickness * 0.5f) / usableExtent;
return ClampSplitterRatio(pointerOptions, mainExtent);
}
inline UISplitterLayoutResult ArrangeUISplitter(
const UIRect& bounds,
UILayoutAxis axis,
float requestedRatio,
const UISplitterConstraints& constraints,
const UISplitterMetrics& metrics) {
UISplitterLayoutOptions options = {};
options.axis = axis;
options.ratio = ClampSplitterRatio(
axis,
requestedRatio,
SplitterDetail::GetMainExtent(bounds, axis),
constraints,
metrics);
options.handleThickness = metrics.thickness;
options.minPrimaryExtent = constraints.primaryMin;
options.minSecondaryExtent = constraints.secondaryMin;
UISplitterLayoutResult result = ArrangeSplitterLayout(options, bounds);
result.splitRatio = options.ratio;
result.resolvedRatio = options.ratio;
return result;
}
} // namespace Layout
} // namespace UI
} // namespace XCEngine