1
0
mirror of https://github.com/vcmi/vcmi.git synced 2026-06-17 22:51:44 +02:00
Files
2026-05-08 23:17:28 +02:00

354 lines
9.8 KiB
C++

/*
* CViewport.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "CViewport.h"
#include "../render/Canvas.h"
#include "../GameEngine.h"
#include "../eventsSDL/InputHandler.h"
CViewport::CViewport(const Rect & viewRect, const Point & contentSz,
CSlider::EStyle style)
: contentSize(contentSz)
, sliderStyle(style)
{
addUsedEvents(TIME | GESTURE);
OBJECT_CONSTRUCTION;
pos.x += viewRect.x;
pos.y += viewRect.y;
pos.w = viewRect.w;
pos.h = viewRect.h;
// Scroll-triggered redraw() must repaint the parent first so that the
// background behind the viewport is cleared before we blit new content.
setRedrawParent(true);
// Content container.
contentHolder = std::make_shared<CIntObject>();
contentHolder->pos.w = contentSz.x;
contentHolder->pos.h = contentSz.y;
// Evaluate which sliders are needed and place them correctly.
// remakeXSlider() is called here for first-time creation too.
updateSliders();
// Prevent double-rendering via the default CIntObject child-forwarding loop.
auto stripRender = [](CIntObject * obj){
obj->recActions &= static_cast<uint8_t>(~(CIntObject::SHOWALL | CIntObject::UPDATE));
};
stripRender(contentHolder.get());
if(vSlider) stripRender(vSlider.get());
if(hSlider) stripRender(hSlider.get());
}
CIntObject * CViewport::content() const
{
return contentHolder.get();
}
// ---------------------------------------------------------------------------
// Slider visibility, sizing & positioning
// ---------------------------------------------------------------------------
void CViewport::remakeVSlider(int length)
{
const int curVal = vSlider ? vSlider->getValue() : 0;
// Destroy old slider (its dtor removes it from children).
vSlider.reset();
{
OBJECT_CONSTRUCTION_TARGETED(this);
vSlider = std::make_shared<CSlider>(
Point(0, 0), // positioned by updateSliders
length,
[this](int val){ onScrollV(val); },
length,
std::max(length + 1, contentSize.y),
curVal,
Orientation::VERTICAL,
sliderStyle);
}
vSlider->setScrollStep(20);
vSlider->setPanningStep(1);
// This slider's GESTURE events are handled exclusively by CViewport::gesturePanning.
// Without this, both the slider (via Scrollable::gesturePanning) and the viewport
// would scroll on every touch-panning event, causing double (over-)scroll speed.
vSlider->removeUsedEvents(GESTURE);
}
void CViewport::remakeHSlider(int length)
{
const int curVal = hSlider ? hSlider->getValue() : 0;
hSlider.reset();
{
OBJECT_CONSTRUCTION_TARGETED(this);
hSlider = std::make_shared<CSlider>(
Point(0, 0),
length,
[this](int val){ onScrollH(val); },
length,
std::max(length + 1, contentSize.x),
curVal,
Orientation::HORIZONTAL,
sliderStyle);
}
hSlider->setScrollStep(20);
hSlider->setPanningStep(1);
// Same reason as vSlider: gesture events are handled by CViewport, not the slider.
hSlider->removeUsedEvents(GESTURE);
}
void CViewport::updateSliders()
{
const bool needV = (contentSize.y > pos.h);
const bool needH = (contentSize.x > pos.w);
// When both bars are needed the corner (SLIDER_W × SLIDER_W) must stay
// empty so the two end-buttons don't overlap. Shorten each bar by
// SLIDER_W to leave that corner free.
const int vLen = pos.h - (needH ? SLIDER_W : 0);
const int hLen = pos.w - (needV ? SLIDER_W : 0);
// Visible content area after sliders are placed.
const int clipW = pos.w - (needV ? SLIDER_W : 0);
const int clipH = pos.h - (needH ? SLIDER_W : 0);
// --- Vertical ---
if(needV)
{
// Recreate if the required length changed.
if(vLen != curVLen)
{
curVLen = vLen;
remakeVSlider(vLen);
}
vSlider->moveTo(Point(pos.x + clipW, pos.y), true);
vSlider->setAmount(contentSize.y);
vSlider->enable();
}
else
{
if(vSlider) vSlider->disable();
}
// --- Horizontal ---
if(needH)
{
if(hLen != curHLen)
{
curHLen = hLen;
remakeHSlider(hLen);
}
hSlider->moveTo(Point(pos.x, pos.y + clipH), true);
hSlider->setAmount(contentSize.x);
hSlider->enable();
}
else
{
if(hSlider) hSlider->disable();
}
// Allow wheel-scroll over the whole viewport body – but only for the
// vertical slider. When both are active the horizontal slider gets only
// its own strip as scrollBounds so the mouse wheel exclusively scrolls
// vertically when the cursor is over the content area.
if(vSlider && !vSlider->isDisabled())
{
// Exclude the hSlider strip: use content clip area, not full pos.
const Rect vScrollArea(pos.x, pos.y, clipW, clipH);
vSlider->setScrollBounds(vScrollArea - vSlider->pos.topLeft());
}
if(hSlider && !hSlider->isDisabled())
{
if(needV)
// Restrict hSlider wheel/gesture to its own strip only.
hSlider->setScrollBounds(Rect(0, 0, hSlider->pos.w, hSlider->pos.h));
else
// Alone: respond everywhere over the viewport.
hSlider->setScrollBounds(pos - hSlider->pos.topLeft());
}
// Strip render bits (enable() resets recActions to ALL_ACTIONS).
auto stripRender = [](CIntObject * obj){
obj->recActions &= static_cast<uint8_t>(~(CIntObject::SHOWALL | CIntObject::UPDATE));
};
if(vSlider) stripRender(vSlider.get());
if(hSlider) stripRender(hSlider.get());
}
Rect CViewport::clipRect() const
{
const bool needV = vSlider && !vSlider->isDisabled();
const bool needH = hSlider && !hSlider->isDisabled();
return Rect(pos.x, pos.y,
pos.w - (needV ? SLIDER_W : 0),
pos.h - (needH ? SLIDER_W : 0));
}
// ---------------------------------------------------------------------------
void CViewport::setContentSize(const Point & sz)
{
contentSize = sz;
contentHolder->pos.w = sz.x;
contentHolder->pos.h = sz.y;
// Reset scroll to origin.
scrollX = 0;
scrollY = 0;
contentHolder->moveTo(pos.topLeft(), true);
if(vSlider) vSlider->scrollTo(0, false);
if(hSlider) hSlider->scrollTo(0, false);
updateSliders();
redraw();
}
void CViewport::fitContentSize()
{
// Compute tight bounding box of all children relative to (0,0) inside
// the content holder. Children have absolute screen coordinates after
// addChild(adjustPosition=true), so subtract contentHolder->pos.topLeft().
int maxX = 0;
int maxY = 0;
const Point origin = contentHolder->pos.topLeft();
for(auto * child : contentHolder->children)
{
const int right = (child->pos.x - origin.x) + child->pos.w;
const int bottom = (child->pos.y - origin.y) + child->pos.h;
if(right > maxX) maxX = right;
if(bottom > maxY) maxY = bottom;
// Ensure every child's redraw() propagates up through the viewport
// clipping chain instead of drawing directly to the screen canvas.
// Without this, hover/click state changes and animation ticks would
// bypass the CanvasClipRectGuard and overdraw outside the viewport.
child->setRedrawParent(true);
}
contentHolder->setRedrawParent(true);
if(maxX <= 0) maxX = 1;
if(maxY <= 0) maxY = 1;
setContentSize(Point(maxX, maxY));
}
void CViewport::scrollToY(int y)
{
if(vSlider && !vSlider->isDisabled())
vSlider->scrollTo(std::max(0, y));
}
void CViewport::onScrollV(int val)
{
const int delta = val - scrollY;
scrollY = val;
inertialPosY = scrollY;
contentHolder->moveBy(Point(0, -delta), true);
redraw();
}
void CViewport::onScrollH(int val)
{
const int delta = val - scrollX;
scrollX = val;
inertialPosX = scrollX;
contentHolder->moveBy(Point(-delta, 0), true);
redraw();
}
// ---------------------------------------------------------------------------
void CViewport::show(Canvas & to)
{
if(vSlider && !vSlider->isDisabled()) vSlider->show(to);
if(hSlider && !hSlider->isDisabled()) hSlider->show(to);
{
CanvasClipRectGuard clip(to, clipRect());
contentHolder->show(to);
}
}
void CViewport::showAll(Canvas & to)
{
if(vSlider && !vSlider->isDisabled()) vSlider->showAll(to);
if(hSlider && !hSlider->isDisabled()) hSlider->showAll(to);
{
CanvasClipRectGuard clip(to, clipRect());
contentHolder->showAll(to);
}
}
bool CViewport::receiveEvent(const Point & position, int eventType) const
{
if(eventType == GESTURE)
return clipRect().isInside(position);
return CIntObject::receiveEvent(position, eventType);
}
void CViewport::gesture(bool on, const Point & initialPosition, const Point & finalPosition)
{
if(on)
{
smoothH.start();
smoothV.start();
inertialPosX = scrollX;
inertialPosY = scrollY;
}
else
{
const uint32_t now = ENGINE->input().getTicks();
smoothH.finish(now);
smoothV.finish(now);
}
}
void CViewport::gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance)
{
const uint32_t ts = ENGINE->input().getTicks();
const int deltaX = lastUpdateDistance.x;
const int deltaY = lastUpdateDistance.y;
smoothH.addStep(deltaX, ts);
smoothV.addStep(deltaY, ts);
// Apply live scroll so content actually moves with the finger.
// Keep inertialPos in sync so post-lift inertia continues from here.
if(hSlider && !hSlider->isDisabled())
hSlider->scrollTo(std::max(0, scrollX + deltaX));
if(vSlider && !vSlider->isDisabled())
vSlider->scrollTo(std::max(0, scrollY + deltaY));
inertialPosX = scrollX; // updated by onScrollH callback above
inertialPosY = scrollY; // updated by onScrollV callback above
}
void CViewport::applyInertialScroll(uint32_t msPassed)
{
if(isGesturing())
return;
inertialPosX += smoothH.tick(msPassed);
inertialPosY += smoothV.tick(msPassed);
const int targetX = static_cast<int>(std::round(inertialPosX));
const int targetY = static_cast<int>(std::round(inertialPosY));
if(hSlider && !hSlider->isDisabled() && targetX != scrollX)
hSlider->scrollTo(std::max(0, targetX));
if(vSlider && !vSlider->isDisabled() && targetY != scrollY)
vSlider->scrollTo(std::max(0, targetY));
}
void CViewport::tick(uint32_t msPassed)
{
applyInertialScroll(msPassed);
}