Added X scrollbar to scroll area, and added toolbar for phone drawing program

This commit is contained in:
Yousef Khadadeh 2026-04-29 15:14:13 +03:00
parent 6a05026f85
commit 74bc17f570
11 changed files with 284 additions and 50 deletions

View file

@ -24,6 +24,7 @@ void GridScrollArea::layout(const Clay_ElementId& id, const Options& o) {
size_t entriesPerRow = std::max<size_t>(rowWidth / options.entryWidth, 1);
size_t rowCount = (options.entryCount + entriesPerRow - 1) / entriesPerRow;
opts.entryCount = rowCount;
opts.scrollbar = options.scrollbar;
uint16_t xPadding = 0;
uint16_t xChildGap = 0;

View file

@ -15,6 +15,7 @@ class GridScrollArea : public Element {
float entryHeight;
size_t entryCount;
bool entryWidthIsMinimum = false;
ScrollArea::ScrollbarType scrollbar = ScrollArea::ScrollbarType::NORMAL;
std::function<void(size_t elementIndex)> elementContent;
std::function<void(const ScrollArea::InnerContentParameters&)> innerContentExtraCallback;
};

View file

@ -2,13 +2,13 @@
#include "../GUIManager.hpp"
#include <Helpers/ConvertVec.hpp>
#include <Helpers/Logger.hpp>
#include "Helpers/MathExtras.hpp"
#include "LayoutElement.hpp"
namespace GUIStuff {
constexpr float TOUCH_SCROLLER_WIDTH = 24.0f;
constexpr float DESKTOP_SCROLLER_WIDTH = 12.0f;
constexpr float SCROLL_WHEEL_SCROLL_SPEED = 20.0f;
constexpr float SCROLL_DAMPING = 0.15f; // Damping on the scroll area velocity when its scrolling on its own
constexpr float SCROLL_MINIMUM_MOTION = 0.15f; // When the scroll area is scrolling on its own, this is the minimum velocity required for it to continue
constexpr float SCROLL_MAX_DAMPING = 0.0001f; // Damping on the scrollAreaMotionMax variable
@ -33,7 +33,7 @@ void ScrollArea::layout(const Clay_ElementId& id, const Options& options) {
auto& bb = boundingBox.value();
clamp_scroll();
if(contentDimensions.y() > bb.height()) {
if((contentDimensions.y() > bb.height() && opts.scrollVertical) || (contentDimensions.x() > bb.width() && opts.scrollHorizontal)) {
gui.in_dynamic_area([&] {
opts.innerContent({.contentDimensions = contentDimensions, .containerDimensions = bb.dim(), .scrollOffset = &scrollOffset});
});
@ -43,8 +43,19 @@ void ScrollArea::layout(const Clay_ElementId& id, const Options& options) {
clamp_scroll();
if(contentDimensions.y() > bb.height() && opts.showScrollbarY)
if(contentDimensions.y() > bb.height() && opts.scrollVertical && opts.scrollbarY != ScrollbarType::NONE)
y_scroll_bar();
else {
yScrollbar.scroller = nullptr;
yScrollbar.scrollbar = nullptr;
}
if(contentDimensions.x() > bb.width() && opts.scrollHorizontal && opts.scrollbarX != ScrollbarType::NONE)
x_scroll_bar();
else {
xScrollbar.scroller = nullptr;
xScrollbar.scrollbar = nullptr;
}
}
}
}
@ -59,7 +70,7 @@ void ScrollArea::y_scroll_bar() {
float areaAboveScrollerSize = scrollerPos * (sAreaDim - scrollerSize);
gui.set_z_index(gui.get_z_index() + 1, [&] {
gui.element<LayoutElement>("y scroll bar", [&] (LayoutElement*, const Clay_ElementId& lId) {
yScrollbar.scrollbar = gui.element<LayoutElement>("y scroll bar", [&] (LayoutElement*, const Clay_ElementId& lId) {
CLAY(lId, {
.layout = {
.sizing = {.width = CLAY_SIZING_FIXED(gui.io.isTouchDevice ? TOUCH_SCROLLER_WIDTH : DESKTOP_SCROLLER_WIDTH), .height = CLAY_SIZING_GROW(0)},
@ -76,9 +87,9 @@ void ScrollArea::y_scroll_bar() {
}
}) {
SkColor4f scrollerColor;
if(isScrollbarHeldY)
if(yScrollbar.isScrollbarHeld)
scrollerColor = gui.io.theme->fillColor1;
else if(isScrollbarHoveredY)
else if(yScrollbar.isScrollbarHovered)
scrollerColor = gui.io.theme->fillColor1;
else
scrollerColor = gui.io.theme->fillColor2;
@ -89,7 +100,7 @@ void ScrollArea::y_scroll_bar() {
}
}) {}
scrollerY = gui.element<LayoutElement>("scroller", [&] (LayoutElement*, const Clay_ElementId& lId2) {
yScrollbar.scroller = gui.element<LayoutElement>("scroller", [&] (LayoutElement*, const Clay_ElementId& lId2) {
CLAY(lId2, {
.layout = {
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(scrollerSize)},
@ -101,7 +112,7 @@ void ScrollArea::y_scroll_bar() {
.layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}}
}) {}
float scrollerWidthPercent = 0.0f;
bool highlighted = isHoveringOverScrollerY || isScrollbarHeldY;
bool highlighted = yScrollbar.isHoveringOverScroller || yScrollbar.isScrollbarHeld || (!gui.io.isTouchDevice && yScrollbar.isScrollbarHovered);
if(gui.io.isTouchDevice)
scrollerWidthPercent = highlighted ? 0.6f : 0.2f;
else
@ -118,55 +129,55 @@ void ScrollArea::y_scroll_bar() {
}
}, LayoutElement::Callbacks{
.onClick = [&, scrollPosMax, sAreaStart, sAreaDim, scrollerSize, areaAboveScrollerSize](LayoutElement* t, const InputManager::MouseButtonCallbackArgs& button) {
bool newIsHoveringOverScrollerY = scrollerY->mouseHovering && (button.deviceType != InputManager::MouseDeviceType::TOUCH || button.down);
bool newIsScrollbarHoveredY = t->mouseHovering && (button.deviceType != InputManager::MouseDeviceType::TOUCH || button.down);
if(isHoveringOverScrollerY != newIsHoveringOverScrollerY) {
isHoveringOverScrollerY = newIsHoveringOverScrollerY;
bool newIsHoveringOverScroller = yScrollbar.scroller->mouseHovering && (button.deviceType != InputManager::MouseDeviceType::TOUCH || button.down);
bool newIsScrollbarHovered = t->mouseHovering && (button.deviceType != InputManager::MouseDeviceType::TOUCH || button.down);
if(yScrollbar.isHoveringOverScroller != newIsHoveringOverScroller) {
yScrollbar.isHoveringOverScroller = newIsHoveringOverScroller;
gui.set_to_layout();
}
if(isScrollbarHoveredY != newIsScrollbarHoveredY) {
isScrollbarHoveredY = newIsScrollbarHoveredY;
if(yScrollbar.isScrollbarHovered != newIsScrollbarHovered) {
yScrollbar.isScrollbarHovered = newIsScrollbarHovered;
gui.set_to_layout();
}
if(button.button == InputManager::MouseButton::LEFT) {
if(button.down && isScrollbarHoveredY) {
isScrollbarHeldY = true;
if(isHoveringOverScrollerY)
scrollerStartPosY = sAreaStart + areaAboveScrollerSize + scrollerSize * 0.5f;
if(button.down && yScrollbar.isScrollbarHovered) {
yScrollbar.isScrollbarHeld = true;
if(yScrollbar.isHoveringOverScroller)
yScrollbar.scrollerStartPos = sAreaStart + areaAboveScrollerSize + scrollerSize * 0.5f;
else
scrollerStartPosY = std::clamp(button.pos.y(), sAreaStart + scrollerSize * 0.5f, sAreaStart + sAreaDim - scrollerSize * 0.5f);
mouseStartPosY = button.pos.y();
yScrollbar.scrollerStartPos = std::clamp(button.pos.y(), sAreaStart + scrollerSize * 0.5f, sAreaStart + sAreaDim - scrollerSize * 0.5f);
yScrollbar.mouseStartPos = button.pos.y();
float newScrollPosFrac;
newScrollPosFrac = std::clamp((scrollerStartPosY - (mouseStartPosY - button.pos.y()) - sAreaStart - scrollerSize * 0.5f) / (sAreaDim - scrollerSize), 0.0f, 1.0f);
newScrollPosFrac = std::clamp((yScrollbar.scrollerStartPos - (yScrollbar.mouseStartPos - button.pos.y()) - sAreaStart - scrollerSize * 0.5f) / (sAreaDim - scrollerSize), 0.0f, 1.0f);
scrollOffset.y() = newScrollPosFrac * (-scrollPosMax);
clamp_scroll();
scrollAreaMotionMax = scrollAreaMotion = {0.0f, 0.0f};
gui.set_to_layout();
}
else if(isScrollbarHeldY) {
isScrollbarHeldY = false;
else if(yScrollbar.isScrollbarHeld) {
yScrollbar.isScrollbarHeld = false;
scrollAreaMotionMax = scrollAreaMotion = {0.0f, 0.0f};
gui.set_to_layout();
}
}
},
.onMotion = [&, scrollPosMax, sAreaStart, sAreaDim, scrollerSize](LayoutElement* t, const InputManager::MouseMotionCallbackArgs& motion) {
bool newIsHoveringOverScrollerY = scrollerY->mouseHovering && motion.deviceType != InputManager::MouseDeviceType::TOUCH;
bool newIsScrollbarHoveredY = t->mouseHovering && motion.deviceType != InputManager::MouseDeviceType::TOUCH;
if(isHoveringOverScrollerY != newIsHoveringOverScrollerY) {
isHoveringOverScrollerY = newIsHoveringOverScrollerY;
bool newIsHoveringOverScroller = yScrollbar.scroller->mouseHovering && motion.deviceType != InputManager::MouseDeviceType::TOUCH;
bool newIsScrollbarHovered = t->mouseHovering && motion.deviceType != InputManager::MouseDeviceType::TOUCH;
if(yScrollbar.isHoveringOverScroller != newIsHoveringOverScroller) {
yScrollbar.isHoveringOverScroller = newIsHoveringOverScroller;
gui.set_to_layout();
}
if(isScrollbarHoveredY != newIsScrollbarHoveredY) {
isScrollbarHoveredY = newIsScrollbarHoveredY;
if(yScrollbar.isScrollbarHovered != newIsScrollbarHovered) {
yScrollbar.isScrollbarHovered = newIsScrollbarHovered;
gui.set_to_layout();
}
if(isScrollbarHeldY) {
if(yScrollbar.isScrollbarHeld) {
float oldScrollOffset = scrollOffset.y();
float newScrollPosFrac;
newScrollPosFrac = std::clamp((scrollerStartPosY - (mouseStartPosY - motion.pos.y()) - sAreaStart - scrollerSize * 0.5f) / (sAreaDim - scrollerSize), 0.0f, 1.0f);
newScrollPosFrac = std::clamp((yScrollbar.scrollerStartPos - (yScrollbar.mouseStartPos - motion.pos.y()) - sAreaStart - scrollerSize * 0.5f) / (sAreaDim - scrollerSize), 0.0f, 1.0f);
scrollOffset.y() = newScrollPosFrac * (-scrollPosMax);
clamp_scroll();
scrollAreaMotionMax = scrollAreaMotion = {0.0f, 0.0f};
@ -178,6 +189,135 @@ void ScrollArea::y_scroll_bar() {
});
}
void ScrollArea::x_scroll_bar() {
float sAreaDim = boundingBox.value().width();
float sAreaStart = boundingBox.value().min.x();
float contDim = contentDimensions.x();
float scrollerSize = (sAreaDim / contDim) * sAreaDim;
float scrollPosMax = contDim - sAreaDim;
float scrollerPos = std::fabs(scrollOffset.x() / scrollPosMax);
float areaAboveScrollerSize = scrollerPos * (sAreaDim - scrollerSize);
gui.set_z_index(gui.get_z_index() + 1, [&] {
xScrollbar.scrollbar = gui.element<LayoutElement>("x scroll bar", [&] (LayoutElement*, const Clay_ElementId& lId) {
CLAY(lId, {
.layout = {
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(gui.io.isTouchDevice ? TOUCH_SCROLLER_WIDTH : DESKTOP_SCROLLER_WIDTH)},
.childAlignment = {.x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_TOP},
.layoutDirection = CLAY_LEFT_TO_RIGHT
},
.floating = {
.zIndex = gui.get_z_index(),
.attachPoints = {
.element = CLAY_ATTACH_POINT_LEFT_BOTTOM,
.parent = CLAY_ATTACH_POINT_LEFT_BOTTOM
},
.attachTo = CLAY_ATTACH_TO_PARENT,
}
}) {
SkColor4f scrollerColor;
if(xScrollbar.isScrollbarHeld)
scrollerColor = gui.io.theme->fillColor1;
else if(xScrollbar.isScrollbarHovered)
scrollerColor = gui.io.theme->fillColor1;
else
scrollerColor = gui.io.theme->fillColor2;
CLAY_AUTO_ID({
.layout = {
.sizing = {.width = CLAY_SIZING_FIXED(areaAboveScrollerSize), .height = CLAY_SIZING_GROW(0)}
}
}) {}
xScrollbar.scroller = gui.element<LayoutElement>("scroller", [&] (LayoutElement*, const Clay_ElementId& lId2) {
CLAY(lId2, {
.layout = {
.sizing = {.width = CLAY_SIZING_FIXED(scrollerSize), .height = CLAY_SIZING_GROW(0)},
.childAlignment = {.x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_TOP},
.layoutDirection = CLAY_TOP_TO_BOTTOM,
}
}) {
CLAY_AUTO_ID({
.layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}}
}) {}
float scrollerWidthPercent = 0.0f;
bool highlighted = xScrollbar.isHoveringOverScroller || xScrollbar.isScrollbarHeld || (!gui.io.isTouchDevice && xScrollbar.isScrollbarHovered);
if(gui.io.isTouchDevice)
scrollerWidthPercent = highlighted ? 0.6f : 0.2f;
else
scrollerWidthPercent = highlighted ? 1.0f : 0.5f;
CLAY_AUTO_ID({
.layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_PERCENT(scrollerWidthPercent)}},
.backgroundColor = convert_vec4<Clay_Color>(highlighted ? gui.io.theme->fillColor1 : gui.io.theme->fillColor2),
.cornerRadius = CLAY_CORNER_RADIUS(3),
}) {}
}
});
CLAY_AUTO_ID({ .layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}}}) {}
}
}, LayoutElement::Callbacks{
.onClick = [&, scrollPosMax, sAreaStart, sAreaDim, scrollerSize, areaAboveScrollerSize](LayoutElement* t, const InputManager::MouseButtonCallbackArgs& button) {
bool newIsHoveringOverScroller = xScrollbar.scroller->mouseHovering && (button.deviceType != InputManager::MouseDeviceType::TOUCH || button.down);
bool newIsScrollbarHovered = t->mouseHovering && (button.deviceType != InputManager::MouseDeviceType::TOUCH || button.down);
if(xScrollbar.isHoveringOverScroller != newIsHoveringOverScroller) {
xScrollbar.isHoveringOverScroller = newIsHoveringOverScroller;
gui.set_to_layout();
}
if(xScrollbar.isScrollbarHovered != newIsScrollbarHovered) {
xScrollbar.isScrollbarHovered = newIsScrollbarHovered;
gui.set_to_layout();
}
if(button.button == InputManager::MouseButton::LEFT) {
if(button.down && xScrollbar.isScrollbarHovered) {
xScrollbar.isScrollbarHeld = true;
if(xScrollbar.isHoveringOverScroller)
xScrollbar.scrollerStartPos = sAreaStart + areaAboveScrollerSize + scrollerSize * 0.5f;
else
xScrollbar.scrollerStartPos = std::clamp(button.pos.x(), sAreaStart + scrollerSize * 0.5f, sAreaStart + sAreaDim - scrollerSize * 0.5f);
xScrollbar.mouseStartPos = button.pos.x();
float newScrollPosFrac;
newScrollPosFrac = std::clamp((xScrollbar.scrollerStartPos - (xScrollbar.mouseStartPos - button.pos.x()) - sAreaStart - scrollerSize * 0.5f) / (sAreaDim - scrollerSize), 0.0f, 1.0f);
scrollOffset.x() = newScrollPosFrac * (-scrollPosMax);
clamp_scroll();
scrollAreaMotionMax = scrollAreaMotion = {0.0f, 0.0f};
gui.set_to_layout();
}
else if(xScrollbar.isScrollbarHeld) {
xScrollbar.isScrollbarHeld = false;
scrollAreaMotionMax = scrollAreaMotion = {0.0f, 0.0f};
gui.set_to_layout();
}
}
},
.onMotion = [&, scrollPosMax, sAreaStart, sAreaDim, scrollerSize](LayoutElement* t, const InputManager::MouseMotionCallbackArgs& motion) {
bool newIsHoveringOverScroller = xScrollbar.scroller->mouseHovering && motion.deviceType != InputManager::MouseDeviceType::TOUCH;
bool newIsScrollbarHovered = t->mouseHovering && motion.deviceType != InputManager::MouseDeviceType::TOUCH;
if(xScrollbar.isHoveringOverScroller != newIsHoveringOverScroller) {
xScrollbar.isHoveringOverScroller = newIsHoveringOverScroller;
gui.set_to_layout();
}
if(xScrollbar.isScrollbarHovered != newIsScrollbarHovered) {
xScrollbar.isScrollbarHovered = newIsScrollbarHovered;
gui.set_to_layout();
}
if(xScrollbar.isScrollbarHeld) {
float oldScrollOffset = scrollOffset.x();
float newScrollPosFrac;
newScrollPosFrac = std::clamp((xScrollbar.scrollerStartPos - (xScrollbar.mouseStartPos - motion.pos.x()) - sAreaStart - scrollerSize * 0.5f) / (sAreaDim - scrollerSize), 0.0f, 1.0f);
scrollOffset.x() = newScrollPosFrac * (-scrollPosMax);
clamp_scroll();
scrollAreaMotionMax = scrollAreaMotion = {0.0f, 0.0f};
if(oldScrollOffset != scrollOffset.x())
gui.set_to_layout();
}
}
});
});
}
void ScrollArea::reset_scroll() {
scrollAreaMotion = scrollAreaMotionMax = scrollOffset = {0.0f, 0.0f};
}
@ -210,13 +350,17 @@ void ScrollArea::update() {
}
void ScrollArea::input_mouse_wheel_callback(const InputManager::MouseWheelCallbackArgs& wheel) {
if(mouseHovering) {
if(mouseHovering || (xScrollbar.scrollbar && xScrollbar.scrollbar->mouseHovering) || (yScrollbar.scrollbar && yScrollbar.scrollbar->mouseHovering)) {
Vector2f oldScrollOffset = scrollOffset;
if(opts.scrollVertical)
scrollOffset.y() = scrollOffset.y() + wheel.amount.y() * 10.0f;
if(opts.scrollHorizontal)
scrollOffset.y() = scrollOffset.x() + wheel.amount.x() * 10.0f;
if(opts.scrollVertical) {
scrollOffset.y() += wheel.amount.y() * SCROLL_WHEEL_SCROLL_SPEED;
if(opts.scrollHorizontal)
scrollOffset.x() += wheel.amount.x() * SCROLL_WHEEL_SCROLL_SPEED;
}
else if(opts.scrollHorizontal)
scrollOffset.x() += wheel.amount.y() * SCROLL_WHEEL_SCROLL_SPEED;
clamp_scroll();
if(oldScrollOffset != scrollOffset)

View file

@ -14,13 +14,19 @@ class ScrollArea : public Element {
Vector2f* scrollOffset;
};
enum class ScrollbarType {
NONE,
NO_INTERACTION,
NORMAL
};
struct Options {
bool scrollVertical = false;
bool scrollHorizontal = false;
bool clipHorizontal = false;
bool clipVertical = false;
bool showScrollbarX = false;
bool showScrollbarY = false;
ScrollbarType scrollbarX = ScrollbarType::NONE;
ScrollbarType scrollbarY = ScrollbarType::NONE;
Clay_LayoutDirection layoutDirection = CLAY_TOP_TO_BOTTOM;
Clay_LayoutAlignmentX xAlign = CLAY_ALIGN_X_LEFT;
Clay_LayoutAlignmentY yAlign = CLAY_ALIGN_Y_TOP;
@ -43,13 +49,21 @@ class ScrollArea : public Element {
Vector2f scrollAreaMotion = {0.0f, 0.0f};
Vector2f scrollAreaMotionMax = {0.0f, 0.0f};
bool isScrollbarHeldY = false;
bool isScrollbarHoveredY = false;
bool isHoveringOverScrollerY = false;
Element* scrollerY = nullptr;
float scrollerStartPosY;
float mouseStartPosY;
struct ScrollbarData {
bool isScrollbarHeld = false;
bool isScrollbarHovered = false;
bool isHoveringOverScroller = false;
Element* scroller = nullptr;
Element* scrollbar = nullptr;
float scrollerStartPos;
float mouseStartPos;
};
ScrollbarData xScrollbar;
ScrollbarData yScrollbar;
void y_scroll_bar();
void x_scroll_bar();
Vector2f contentDimensions = {0.0f, 0.0f};
Vector2f scrollOffset = {0.0f, 0.0f};