312 lines
11 KiB
C++
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
|