Files
ppsspp/Common/UI/ScrollView.cpp
T
Henrik RydgårdandClaude Opus 5.5 8e353761c7 UI: Let scroll views extend under the bottom inset when they don't quite reach the edge
A vertical ScrollView that extends into the bottom inset (like a navigation bar) adds
that much space after its contents, so the end can be scrolled out from under it.
It only did that when the view reached the very bottom of the screen, and then added
the whole inset. Since 58fa522d48 inset the content area of dialog screens on
screens over 600 dp, a phone like the S21 Ultra (617 dp wide) got a 1.7 dp inset,
which turned the extra space off. Add exactly the part of the view that lies in the
inset instead, and skip the bottom shadow in that case.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 09:41:38 +01:00

718 lines
21 KiB
C++

#include <algorithm>
#include "Common/System/System.h"
#include "Common/System/Display.h"
#include "Common/Data/Color/RGBAUtil.h"
#include "Common/UI/Context.h"
#include "Common/UI/ScrollView.h"
#include "Common/Data/Text/I18n.h"
#include "Common/Log.h"
#include "Common/StringUtils.h"
#include "Common/TimeUtil.h"
namespace UI {
float ScrollView::lastScrollPosX = 0;
float ScrollView::lastScrollPosY = 0;
// How long the scroll bob stays after the last activity, and how fast it fades in and out.
static constexpr double bobHoldTime = 1.0;
static constexpr float bobFadeInTime = 0.1f;
static constexpr float bobFadeOutTime = 0.5f;
// Width of the strip along the right edge where the bob can be grabbed, and where hovering shows it.
static constexpr float bobGrabWidth = 20.0f;
ScrollView::ScrollView(Orientation orientation, LayoutParams *layoutParams)
: ViewGroup(layoutParams), orientation_(orientation) {
if (orientation == ORIENT_HORIZONTAL) {
gesture_.SetGestureMask(GESTURE_DRAG_HORIZONTAL);
} else {
gesture_.SetGestureMask(GESTURE_DRAG_VERTICAL);
}
// Show the bob briefly when the view appears, so it's clear that it scrolls.
lastScrollActivity_ = time_now_d();
lastBobUpdate_ = lastScrollActivity_;
}
ScrollView::~ScrollView() {
lastScrollPosX = 0;
lastScrollPosY = 0;
}
Margins ScrollView::GetMargins() const {
if (views_.size()) {
const LinearLayoutParams *linLayoutParams = views_[0]->GetLayoutParams()->As<LinearLayoutParams>();
if (linLayoutParams) {
return linLayoutParams->margins;
}
}
return Margins(0);
}
void ScrollView::Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert) {
// Respect margins
const Margins margins = GetMargins();
// The scroll view itself simply obeys its parent - but also tries to fit the child if possible.
MeasureBySpec(layoutParams_->width, horiz.size, horiz, &measuredWidth_);
MeasureBySpec(layoutParams_->height, vert.size, vert, &measuredHeight_);
if (views_.size()) {
if (orientation_ == ORIENT_HORIZONTAL) {
MeasureSpec v = MeasureSpec(AT_MOST, measuredHeight_ - margins.vert());
if (measuredHeight_ == 0.0f && (vert.type == UNSPECIFIED || layoutParams_->height == WRAP_CONTENT)) {
v.type = UNSPECIFIED;
}
views_[0]->Measure(dc, MeasureSpec(UNSPECIFIED, measuredWidth_), v);
MeasureBySpec(layoutParams_->height, views_[0]->GetMeasuredHeight() + margins.vert(), vert, &measuredHeight_);
if (layoutParams_->width == WRAP_CONTENT)
MeasureBySpec(layoutParams_->width, views_[0]->GetMeasuredWidth() + margins.horiz(), horiz, &measuredWidth_);
} else {
MeasureSpec h = MeasureSpec(AT_MOST, measuredWidth_ - margins.horiz());
if (measuredWidth_ == 0.0f && (horiz.type == UNSPECIFIED || layoutParams_->width == WRAP_CONTENT)) {
h.type = UNSPECIFIED;
}
views_[0]->Measure(dc, h, MeasureSpec(UNSPECIFIED, measuredHeight_));
MeasureBySpec(layoutParams_->width, views_[0]->GetMeasuredWidth() + margins.horiz(), horiz, &measuredWidth_);
if (layoutParams_->height == WRAP_CONTENT)
MeasureBySpec(layoutParams_->height, views_[0]->GetMeasuredHeight() + margins.vert(), vert, &measuredHeight_);
}
// The below seems misguided and leads to wrong sized scrollviews. Need to investigate if it's needed somewhere...
/*
if (orientation_ == ORIENT_VERTICAL && vert.type != EXACTLY) {
float bestHeight = std::max(views_[0]->GetMeasuredHeight(), views_[0]->GetBounds().h);
if (vert.type == AT_MOST)
bestHeight = std::min(bestHeight, vert.size);
if (measuredHeight_ < bestHeight && layoutParams_->height < 0.0f) {
measuredHeight_ = bestHeight;
}
}*/
}
}
void ScrollView::Layout() {
if (!views_.size())
return;
Bounds scrolled;
// The child was measured inside its margins.
const Margins margins = GetMargins();
scrolled.w = views_[0]->GetMeasuredWidth();
scrolled.h = views_[0]->GetMeasuredHeight();
layoutScrollPos_ = ClampedScrollPos(scrollPos_);
switch (orientation_) {
case ORIENT_HORIZONTAL:
if (scrolled.w != lastViewSize_) {
if (rememberPos_)
scrollPos_ = *rememberPos_;
lastViewSize_ = scrolled.w;
}
scrolled.x = bounds_.x + margins.left - layoutScrollPos_;
scrolled.y = bounds_.y + margins.top;
break;
case ORIENT_VERTICAL:
if (scrolled.h != lastViewSize_) {
if (rememberPos_)
scrollPos_ = *rememberPos_;
lastViewSize_ = scrolled.h;
}
scrolled.x = bounds_.x + margins.left;
scrolled.y = bounds_.y + margins.top - layoutScrollPos_;
break;
}
views_[0]->SetBounds(scrolled);
views_[0]->Layout();
}
bool ScrollView::Key(const KeyInput &input) {
if (visibility_ != V_VISIBLE)
return ViewGroup::Key(input);
float scrollSpeed = 250;
switch (input.deviceId) {
case DEVICE_ID_XR_CONTROLLER_LEFT:
case DEVICE_ID_XR_CONTROLLER_RIGHT:
scrollSpeed = 50;
break;
default:
break;
}
if ((input.flags & KeyInputFlags::DOWN) && mouseHover_) {
if ((input.keyCode == NKCODE_EXT_MOUSEWHEEL_UP || input.keyCode == NKCODE_EXT_MOUSEWHEEL_DOWN) &&
(input.flags & KeyInputFlags::HAS_WHEEL_DELTA)) {
scrollSpeed = (float)((s32)input.flags >> 16) * 1.25f; // Fudge factor. TODO: Should be moved to the backends.
}
switch (input.keyCode) {
case NKCODE_EXT_MOUSEWHEEL_UP:
ScrollRelative(-scrollSpeed);
break;
case NKCODE_EXT_MOUSEWHEEL_DOWN:
ScrollRelative(scrollSpeed);
break;
default:
break;
}
}
return ViewGroup::Key(input);
}
const float friction = 0.92f;
const float stop_threshold = 0.1f;
bool ScrollView::Touch(const TouchInput &input) {
if (input.flags & TouchInputFlags::MOUSE) {
// Kinda hacky, we should make a proper hover mechanism.
mouseHover_ = bounds_.Contains(input.x, input.y);
bobHover_ = mouseHover_ && orientation_ == ORIENT_VERTICAL && input.x >= bounds_.x2() - bobGrabWidth;
}
// Only the left button scrolls, but the children still get the rest, so they can see hovers and other buttons.
if ((input.flags & TouchInputFlags::MOUSE) && (input.buttons & 1) == 0) {
return ViewGroup::Touch(input);
}
if ((input.flags & TouchInputFlags::DOWN) && scrollTouchId_ == -1 && bounds_.Contains(input.x, input.y)) {
if (orientation_ == ORIENT_VERTICAL) {
Bob bob = ComputeBob();
float internalY = input.y - bounds_.y;
float bobMargin = 3.0f; // Add some extra margin for the touch.
draggingBob_ = internalY >= bob.offset - bobMargin && internalY <= bob.offset + bob.size + bobMargin && input.x >= bounds_.x2() - bobGrabWidth;
barDragStart_ = bob.offset;
barDragOffset_ = internalY - bob.offset;
}
scrollStart_ = scrollPos_;
inertia_ = 0.0f;
scrollTouchId_ = input.id;
}
Gesture gesture = orientation_ == ORIENT_VERTICAL ? GESTURE_DRAG_VERTICAL : GESTURE_DRAG_HORIZONTAL;
if ((input.flags & TouchInputFlags::UP) && input.id == scrollTouchId_) {
float info[4];
if (gesture_.GetGestureInfo(gesture, input.id, info)) {
inertia_ = info[1];
}
scrollTouchId_ = -1;
draggingBob_ = false;
}
// We modify the input2 we send to children, so we can cancel drags if we start scrolling, and stuff like that.
TouchInput input2;
if (CanScroll()) {
if (draggingBob_) {
// Cancel any drags/holds on the children instantly to avoid accidental click-throughs.
input2.flags = TouchInputFlags::UP | TouchInputFlags::CANCEL;
// Skip the gesture manager, do calculations directly.
// Might switch to the gesture later.
Bob bob = ComputeBob();
float internalY = input.y - bounds_.y;
float bobPos = internalY - barDragOffset_;
float bobDragMax = bounds_.h - bob.size;
float newScrollPos = Clamp(bobPos / bobDragMax, 0.0f, 1.0f) * bob.scrollMax;
scrollPos_ = newScrollPos;
scrollTarget_ = newScrollPos;
scrollToTarget_ = false;
} else {
input2 = gesture_.Update(input, bounds_);
float info[4];
if (input.id == scrollTouchId_ && gesture_.GetGestureInfo(gesture, input.id, info) && !(input.flags & TouchInputFlags::DOWN)) {
float pos = scrollStart_ - info[0];
scrollPos_ = pos;
scrollTarget_ = pos;
scrollToTarget_ = false;
}
}
} else {
input2 = input;
scrollTarget_ = scrollPos_;
scrollToTarget_ = false;
}
if (!(input.flags & TouchInputFlags::DOWN) || bounds_.Contains(input.x, input.y)) {
return ViewGroup::Touch(input2);
} else {
return false;
}
}
float ScrollView::BottomInsetOverlap() const {
const float insetStart = g_display.dp_yres - System_GetPropertyFloat(SYSPROP_DISPLAY_SAFE_INSET_BOTTOM);
return std::max(0.0f, bounds_.y2() - insetStart);
}
float ScrollView::ChildSize() const {
float extraSpace = 0.0f;
if (orientation_ == ORIENT_VERTICAL) {
// Let the end of the contents scroll up out of the bottom inset.
extraSpace = BottomInsetOverlap();
}
if (views_.empty()) {
// Avoid division by zero. This shouldn't happen, anyway.
return 1.0f;
}
const Margins margins = GetMargins();
const float marginSize = orientation_ == ORIENT_VERTICAL ? margins.vert() : margins.horiz();
return std::max(0.01f, views_[0]->GetBounds().GetSize(orientation_) + marginSize + extraSpace);
}
float ScrollView::ScrollMax() const {
const float childHeight = ChildSize();
return std::max(0.0f, childHeight - bounds_.GetSize(orientation_));
}
ScrollView::Bob ScrollView::ComputeBob() const {
Bob bob{};
if (views_.empty()) {
return bob;
}
const float childHeight = ChildSize();
const float ratio = bounds_.h / childHeight;
const float scrollMax = ScrollMax();
if (ratio < 1.0f && scrollMax > 0.0f) {
bob.show = true;
bob.thickness = draggingBob_ ? 15.0f : 6.0f;
bob.size = ratio * bounds_.h;
bob.offset = (HardClampedScrollPos(scrollPos_) / scrollMax) * (bounds_.h - bob.size);
bob.scrollMax = scrollMax;
}
return bob;
}
void ScrollView::Draw(UIContext &dc) {
if (!views_.size()) {
ViewGroup::Draw(dc);
return;
}
dc.PushScissor(bounds_);
dc.FillRect(bg_, bounds_);
// For debugging layout issues, this can be useful.
// dc.FillRect(Drawable(0x60FF00FF), bounds_);
views_[0]->Draw(dc);
// If not anchored at the top of the screen exactly, and not scrolled to the top,
// draw a subtle drop shadow to indicate scrollability.
constexpr float darkness = 0.4f;
if (shadows_ && bounds_.y > 0.0f && orientation_ == ORIENT_VERTICAL) {
float radius = 20.0f;
Bounds shadowBounds = bounds_;
shadowBounds.x -= radius;
shadowBounds.w += radius * 2;
shadowBounds.y -= radius * 2;
shadowBounds.h = radius * 2;
float fade = std::clamp(scrollPos_ * 0.05f, 0.0f, 1.0f) * darkness;
dc.DrawRectDropShadow(shadowBounds, radius, fade);
}
// Same at the bottom. (we check against the common UI layout mode)
const float y2 = dc.GetLayoutBounds(ViewLayoutMode::IgnoreBottomInset, false).y2();
if (shadows_ && bounds_.y2() < y2 && BottomInsetOverlap() <= 0.0f && orientation_ == ORIENT_VERTICAL) {
float radius = 20.0f;
Bounds shadowBounds = bounds_;
shadowBounds.x -= radius;
shadowBounds.w += radius * 2;
shadowBounds.y = bounds_.y2() - radius * 2;
shadowBounds.h = radius * 2;
float fade = std::clamp((ScrollMax() - scrollPos_) * 0.05f, 0.0f, 1.0f) * darkness;
dc.DrawRectDropShadow(shadowBounds, radius, fade);
}
// If there are any additional views, we treat them as overlays attached at the top - they don't scroll, and they're
// drawn on top of the shadow. TODO: Give them an idea about the scroll position so they can fade in?
for (size_t i = 1; i < views_.size(); ++i) {
views_[i]->Draw(dc);
}
dc.PopScissor();
// Vertical scroll bob. We don't support a horizontal yet.
if (orientation_ != ORIENT_VERTICAL) {
return;
}
Bob bob = ComputeBob();
if (bob.show && bobAlpha_ > 0.0f) {
Bounds bobBounds(bounds_.x2() - bob.thickness, bounds_.y + bob.offset, bob.thickness, bob.size);
uint32_t scrollBarColor = dc.GetTheme().scrollbarColor;
if (draggingBob_) {
scrollBarColor |= 0xFF000000;
}
dc.FillRect(Drawable(alphaMul(scrollBarColor, bobAlpha_)), bobBounds);
}
}
bool ScrollView::SubviewFocused(View *view) {
if (!ViewGroup::SubviewFocused(view))
return false;
const Bounds &vBounds = view->GetBounds();
// Scroll so that the focused view is visible, and a bit more so that headers etc gets visible too, in most cases.
const float overscroll = std::min(view->GetBounds().h / 1.5f, GetBounds().h / 4.0f);
float pos = ClampedScrollPos(scrollPos_);
float visibleSize = orientation_ == ORIENT_VERTICAL ? bounds_.h : bounds_.w;
float visibleEnd = scrollPos_ + visibleSize;
float viewStart = 0.0f, viewEnd = 0.0f;
switch (orientation_) {
case ORIENT_HORIZONTAL:
viewStart = layoutScrollPos_ + vBounds.x - bounds_.x;
viewEnd = layoutScrollPos_ + vBounds.x2() - bounds_.x;
break;
case ORIENT_VERTICAL:
viewStart = layoutScrollPos_ + vBounds.y - bounds_.y;
viewEnd = layoutScrollPos_ + vBounds.y2() - bounds_.y;
break;
}
if (viewEnd > visibleEnd) {
ScrollTo(viewEnd - visibleSize + overscroll);
} else if (viewStart < pos) {
ScrollTo(viewStart - overscroll);
}
return true;
}
NeighborResult ScrollView::FindScrollNeighbor(View *view, const Point2D &target, FocusMove direction, NeighborResult best) {
if (ContainsSubview(view) && views_[0]->IsViewGroup()) {
ViewGroup *vg = static_cast<ViewGroup *>(views_[0]);
int found = -1;
for (int i = 0, n = vg->GetNumSubviews(); i < n; ++i) {
View *child = vg->GetViewByIndex(i);
if (child == view || child->ContainsSubview(view)) {
found = i;
break;
}
}
// Okay, the previously focused view is inside this.
if (found != -1) {
float mult = 0.0f;
switch (direction) {
case FocusMove::PREV_PAGE:
mult = -1.0f;
break;
case FocusMove::NEXT_PAGE:
mult = 1.0f;
break;
default:
break;
}
// Okay, now where is our ideal target?
Point2D targetPos = view->GetBounds().Center();
if (orientation_ == ORIENT_VERTICAL)
targetPos.y += mult * bounds_.h;
else
targetPos.x += mult * bounds_.w;
// Okay, which subview is closest to that?
best = vg->FindScrollNeighbor(view, targetPos, direction, best);
// Avoid reselecting the same view.
if (best.view == view)
best.view = nullptr;
return best;
}
}
return ViewGroup::FindScrollNeighbor(view, target, direction, best);
}
void ScrollView::PersistData(PersistStatus status, std::string anonId, PersistMap &storage) {
ViewGroup::PersistData(status, anonId, storage);
std::string tag = Tag();
if (tag.empty()) {
tag = anonId;
}
PersistBuffer &buffer = storage["ScrollView::" + tag];
switch (status) {
case PERSIST_SAVE:
{
buffer.resize(1);
float pos = scrollToTarget_ ? scrollTarget_ : scrollPos_;
// Hmm, ugly... better buffer?
buffer[0] = *(int *)&pos;
}
break;
case PERSIST_RESTORE:
if (buffer.size() == 1) {
float pos = *(float *)&buffer[0];
scrollPos_ = pos;
scrollTarget_ = pos;
scrollToTarget_ = false;
}
break;
}
}
void ScrollView::SetVisibility(Visibility visibility) {
if (visibility == V_VISIBLE && visibility_ != V_VISIBLE) {
lastScrollActivity_ = time_now_d();
}
ViewGroup::SetVisibility(visibility);
if (visibility == V_GONE && !rememberPos_) {
// Since this is no longer shown, forget the scroll position.
// For example, this happens when switching tabs.
ScrollTo(0.0f);
}
}
void ScrollView::ScrollTo(float newScrollPos) {
scrollTarget_ = newScrollPos;
scrollToTarget_ = true;
}
void ScrollView::ScrollRelative(float distance) {
scrollTarget_ = scrollPos_ + distance;
scrollToTarget_ = true;
}
float ScrollView::HardClampedScrollPos(float pos) const {
if (!views_.size() || bounds_.h == 0.0f) {
return 0.0f;
}
const float childSize = ChildSize();
const float containerSize = bounds_.GetSize(orientation_);
const float scrollMax = std::max(0.0f, childSize - containerSize);
return Clamp(pos, 0.0f, scrollMax);
}
float ScrollView::ClampedScrollPos(float pos) {
if (!views_.size() || bounds_.h == 0.0f) {
return 0.0f;
}
float childSize = ChildSize();
float containerSize = bounds_.GetSize(orientation_);
float scrollMax = std::max(0.0f, childSize - containerSize);
Gesture gesture = orientation_ == ORIENT_VERTICAL ? GESTURE_DRAG_VERTICAL : GESTURE_DRAG_HORIZONTAL;
// TODO: Not all of this is properly orientation independent.
if (scrollTouchId_ >= 0 && gesture_.IsGestureActive(gesture, scrollTouchId_) && bounds_.h > 0.0f) {
float maxPull = bounds_.h * 0.1f;
if (pos < 0.0f) {
float dist = std::min(-pos * (1.0f / bounds_.h), 1.0f);
pull_ = -(sqrtf(dist) * maxPull);
} else if (pos > scrollMax) {
float dist = std::min((pos - scrollMax) * (1.0f / bounds_.h), 1.0f);
pull_ = sqrtf(dist) * maxPull;
} else {
pull_ = 0.0f;
}
}
if (pos < 0.0f && pos < pull_) {
pos = pull_;
}
if (pos > scrollMax && pos > scrollMax + pull_) {
pos = scrollMax + pull_;
}
if (childSize < containerSize &&alignOpposite_) {
pos = -(containerSize - childSize);
}
return pos;
}
void ScrollView::ScrollToBottom() {
const float childSize = ChildSize();
const float scrollMax = std::max(0.0f, childSize - bounds_.GetSize(orientation_));
scrollPos_ = scrollMax;
scrollTarget_ = scrollMax;
}
bool ScrollView::CanScroll() const {
if (!views_.size())
return false;
return ChildSize() > bounds_.GetSize(orientation_);
}
void ScrollView::GetLastScrollPosition(float &x, float &y) {
x = lastScrollPosX;
y = lastScrollPosY;
}
void ScrollView::Update() {
if (visibility_ != V_VISIBLE) {
inertia_ = 0.0f;
}
ViewGroup::Update();
float oldPos = scrollPos_;
Gesture gesture = orientation_ == ORIENT_VERTICAL ? GESTURE_DRAG_VERTICAL : GESTURE_DRAG_HORIZONTAL;
gesture_.UpdateFrame();
if (scrollToTarget_) {
float target = ClampedScrollPos(scrollTarget_);
inertia_ = 0.0f;
if (fabsf(target - scrollPos_) < 0.5f) {
scrollPos_ = target;
scrollToTarget_ = false;
} else {
scrollPos_ += (target - scrollPos_) * 0.3f;
}
} else if (inertia_ != 0.0f && !gesture_.IsGestureActive(gesture, scrollTouchId_)) {
scrollPos_ -= inertia_;
inertia_ *= friction;
if (fabsf(inertia_) < stop_threshold)
inertia_ = 0.0f;
}
if (!gesture_.IsGestureActive(gesture, scrollTouchId_)) {
scrollPos_ = ClampedScrollPos(scrollPos_);
pull_ *= friction;
if (fabsf(pull_) < 0.01f) {
pull_ = 0.0f;
}
}
if (oldPos != scrollPos_)
orientation_ == ORIENT_HORIZONTAL ? lastScrollPosX = scrollPos_ : lastScrollPosY = scrollPos_;
// We load some lists asynchronously, so don't update the position until it's loaded.
if (rememberPos_ && ClampedScrollPos(scrollPos_) != ClampedScrollPos(*rememberPos_)) {
*rememberPos_ = scrollPos_;
}
const double now = time_now_d();
const float dt = std::min((float)(now - lastBobUpdate_), 0.1f);
lastBobUpdate_ = now;
if (scrollPos_ != oldPos || draggingBob_ || scrollTouchId_ != -1 || bobHover_) {
lastScrollActivity_ = now;
}
if (now - lastScrollActivity_ < bobHoldTime) {
bobAlpha_ = std::min(1.0f, bobAlpha_ + dt / bobFadeInTime);
} else {
bobAlpha_ = std::max(0.0f, bobAlpha_ - dt / bobFadeOutTime);
}
}
ListView::ListView(ListAdaptor *a, std::set<int> hidden, std::map<int, ImageID> icons, LayoutParams *layoutParams)
: ScrollView(ORIENT_VERTICAL, layoutParams), adaptor_(a), maxHeight_(0), hidden_(hidden), icons_(icons) {
linLayout_ = new LinearLayout(ORIENT_VERTICAL);
linLayout_->SetSpacing(0.0f);
Add(linLayout_);
CreateAllItems();
}
void ListView::CreateAllItems() {
linLayout_->Clear();
// Let's not be clever yet, we'll just create them all up front and add them all in.
for (int i = 0; i < adaptor_->GetNumItems(); i++) {
if (hidden_.count(i)) {
// Item was hidden, skip it.
continue;
}
ImageID *imageID = nullptr;
auto iter = icons_.find(i);
if (iter != icons_.end()) {
imageID = &iter->second;
}
const bool selected = adaptor_->GetSelected() == i;
View *v = linLayout_->Add(adaptor_->CreateItemView(i, selected, imageID));
adaptor_->AddEventCallback(v, [this, i](UI::EventParams &e) {
OnItemCallback(i, e);
});
}
}
void ListView::Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert) {
ScrollView::Measure(dc, horiz, vert);
if (maxHeight_ > 0 && measuredHeight_ > maxHeight_) {
measuredHeight_ = maxHeight_;
}
}
std::string ListView::DescribeText() const {
auto u = GetI18NCategory(I18NCat::UI_ELEMENTS);
return DescribeListOrdered(u->T("List:"));
}
void ListView::OnItemCallback(int num, EventParams &e) {
EventParams ev{};
ev.v = nullptr;
ev.a = num;
adaptor_->SetSelected(num);
OnChoice.Trigger(ev);
CreateAllItems();
}
View *ChoiceListAdaptor::CreateItemView(int index, bool selected, ImageID *optionalImageID) {
Choice *choice;
std::string title = items_[index];
if (default_ == index) {
auto di = GetI18NCategory(I18NCat::DIALOG);
title = ApplySafeSubstitutions("%1 (%2)", title, di->T("Default"));
}
if (optionalImageID) {
choice = new Choice(title, *optionalImageID);
} else {
choice = new Choice(title);
}
if (selected) {
choice->SetSelectedIndicator(true);
}
return choice;
}
void ChoiceListAdaptor::AddEventCallback(View *view, std::function<void(EventParams &)> callback) {
Clickable *choice = dynamic_cast<Clickable *>(view);
if (choice) {
choice->OnClick.Add(callback);
}
}
View *StringVectorListAdaptor::CreateItemView(int index, bool selected, ImageID *optionalImageID) {
std::string title = items_[index];
if (index == default_) {
auto di = GetI18NCategory(I18NCat::DIALOG);
title = ApplySafeSubstitutions("%1 (%2)", title, di->T("Default"));
}
Choice *choice = new Choice(title, optionalImageID ? *optionalImageID : ImageID());
if (selected) {
choice->SetSelectedIndicator(true);
}
return choice;
}
void StringVectorListAdaptor::AddEventCallback(View *view, std::function<void(EventParams &)> callback) {
Clickable *choice = dynamic_cast<Clickable *>(view);
if (choice) {
choice->OnClick.Add(callback);
}
}
} // namespace