view foosdk/sdk/libPPUI/CListControl.cpp @ 1:20d02a178406 default tip

*: check in everything else yay
author Paper <paper@tflc.us>
date Mon, 05 Jan 2026 02:15:46 -0500
parents
children
line wrap: on
line source

#include "stdafx.h"
#include "CListControl.h"
#include "PaintUtils.h"
#include "CListControlUserOptions.h"
#include "GDIUtils.h"
#include "DarkMode.h"

#define PrepLayoutCache_Debug 0
#define Scroll_Debug 0

#if Scroll_Debug
#define Scroll_Debug_Print(...) PFC_DEBUG_PRINT_FORCED(__VA_ARGS__)
#else
#define Scroll_Debug_Print(...)
#endif

CListControlUserOptions * CListControlUserOptions::instance = nullptr;

CRect CListControlImpl::GetClientRectHook() const {
	CRect temp; 
	if ( m_hWnd == NULL || !GetClientRect(temp)) temp.SetRectEmpty(); 
	return temp;
}

bool CListControlImpl::UserEnabledSmoothScroll() const {
	auto i = CListControlUserOptions::instance;
	if ( i != nullptr ) return i->useSmoothScroll();
	return false;
}

LRESULT CListControlImpl::SetFocusPassThru(UINT,WPARAM,LPARAM,BOOL& bHandled) {
	SetFocus();
	bHandled = FALSE;
	return 0;
}

void CListControlImpl::EnsureVisibleRectAbs(const CRect & p_rect) {
	const CRect rcView = GetVisibleRectAbs();
	const CRect rcItem = p_rect;
	int deltaX = 0, deltaY = 0;

	const bool centerOnItem = m_ensureVisibleUser;

	if (rcItem.top < rcView.top || rcItem.bottom > rcView.bottom) {
		if (rcItem.Height() > rcView.Height()) {
			deltaY = rcItem.top - rcView.top;
		} else {
			if (centerOnItem) {
				deltaY = rcItem.CenterPoint().y - rcView.CenterPoint().y;
			} else {
				if (rcItem.bottom > rcView.bottom) deltaY = rcItem.bottom - rcView.bottom;
				else deltaY = rcItem.top - rcView.top;
				
			}
		}
	}
	if (rcItem.left < rcView.left || rcItem.right > rcView.right) {
		if (rcItem.Width() > rcView.Width()) {
			if (rcItem.left > rcView.left || rcItem.right < rcView.right) deltaX = rcItem.left - rcView.left;
		} else {
			if (centerOnItem) {
				deltaX = rcItem.CenterPoint().x - rcView.CenterPoint().x;
			} else {
				if (rcItem.right > rcView.right) deltaX = rcItem.right - rcView.right;
				else deltaX = rcItem.left - rcView.left;
			}
		}
	}

	if (deltaX != 0 || deltaY != 0) {
		MoveViewOriginDelta(CPoint(deltaX,deltaY));
	}
}
void CListControlImpl::EnsureItemVisible(t_size p_item, bool bUser) {
	m_ensureVisibleUser = bUser;
	PFC_ASSERT(p_item < GetItemCount());
	if (this->PrepLayoutCache(m_viewOrigin, p_item, p_item+1 )) {
		RefreshSliders(); Invalidate();
	}
	EnsureVisibleRectAbs(GetItemRectAbs(p_item));
	m_ensureVisibleUser = false;
}
void CListControlImpl::EnsureHeaderVisible2(size_t atItem) {
	CRect rect;
	if (GetGroupHeaderRectAbs2(atItem,rect)) EnsureVisibleRectAbs(rect);
}

void CListControlImpl::RefreshSlider(bool p_vertical) {
	const CRect viewArea = GetViewAreaRectAbs();
	const CRect rcVisible = GetVisibleRectAbs();
	SCROLLINFO si = {};
	si.cbSize = sizeof(si);
	si.fMask = SIF_PAGE|SIF_RANGE|SIF_POS;


	if (AllowScrollbar(p_vertical)) {
		if (p_vertical) {
			si.nPage = rcVisible.Height();
			si.nMin = viewArea.top;
			si.nMax = viewArea.bottom - 1;
			si.nPos = rcVisible.top;
		} else {
			si.nPage = rcVisible.Width();
			si.nMin = viewArea.left;
			si.nMax = viewArea.right - 1;
			si.nPos = rcVisible.left;
		}	
	}

	Scroll_Debug_Print("RefreshSlider vertical=", p_vertical, ", nPage=", si.nPage, ", nMin=", si.nMin, ", nMax=", si.nMax, ", nPos=", si.nPos);

	SetScrollInfo(p_vertical ? SB_VERT : SB_HORZ, &si);
}

void CListControlImpl::RefreshSliders() {
	//PROBLEM: while lots of data can be reused across those, it has to be recalculated inbetween because view area etc may change when scroll info changes
	RefreshSlider(false); RefreshSlider(true);
}

int CListControlImpl::GetScrollThumbPos(int which) {
	SCROLLINFO si = {};
	si.cbSize = sizeof(si);
	si.fMask = SIF_TRACKPOS;
	WIN32_OP_D( GetScrollInfo(which,&si) );
	return si.nTrackPos;
}

bool CListControlImpl::ResolveGroupRangeCached(size_t itemInGroup, size_t& outBegin, size_t& outEnd) const {
	auto end = this->m_groupHeaders.upper_bound(itemInGroup);
	if (end == this->m_groupHeaders.begin()) return false;
	auto begin = end; --begin;
	outBegin = *begin;
	if (end == this->m_groupHeaders.end()) outEnd = this->GetItemCount();
	else outEnd = *end;
	return true;
}

size_t CListControlImpl::ResolveGroupRange2(t_size p_base) const {
	const auto id = this->GetItemGroup(p_base);
	const size_t count = this->GetItemCount();
	size_t walk = p_base + 1;
	while (walk < count && GetItemGroup(walk) == id) ++walk;
	return walk - p_base;
}


static int HandleScroll(WORD p_code,int p_offset,int p_page, int p_line, int p_bottom, int p_thumbpos) {
	switch(p_code) {
	case SB_LINEUP:
		return p_offset - p_line;
	case SB_LINEDOWN:
		return p_offset + p_line;
	case SB_BOTTOM:
		return p_bottom - p_page;
	case SB_TOP:
		return 0;
	case SB_PAGEUP:
		return p_offset - p_page;
	case SB_PAGEDOWN:
		return p_offset + p_page;
	case SB_THUMBPOSITION:
		return p_thumbpos;
	case SB_THUMBTRACK:
		return p_thumbpos;
	default:
		return p_offset;
	}
}

static CPoint ClipPointToRect(CPoint const & p_pt,CRect const & p_rect) {
	return CPoint(pfc::clip_t(p_pt.x,p_rect.left,p_rect.right),pfc::clip_t(p_pt.y,p_rect.top,p_rect.bottom));
}

void CListControlImpl::MoveViewOriginNoClip(CPoint p_target) {
	UpdateWindow();
	PrepLayoutCache(p_target);
	const CPoint old = m_viewOrigin;
	m_viewOrigin = p_target;

	if (m_viewOrigin != old) {
#if PrepLayoutCache_Debug
		PFC_DEBUGLOG << "MoveViewOriginNoClip: m_viewOrigin=" << m_viewOrigin.x << "," << m_viewOrigin.y;
#endif
		
		if (m_viewOrigin.x != old.x) SetScrollPos(SB_HORZ,m_viewOrigin.x);
		if (m_viewOrigin.y != old.y) SetScrollPos(SB_VERT,m_viewOrigin.y);

		const CPoint delta = old - m_viewOrigin;
		if (FixedOverlayPresent()) Invalidate();
		else {
			DWORD flags = SW_INVALIDATE | SW_ERASE;
			const DWORD smoothScrollMS = 50;
			if (this->UserEnabledSmoothScroll() && this->CanSmoothScroll()) {
				flags |= SW_SMOOTHSCROLL | (smoothScrollMS << 16);
			}

			ScrollWindowEx(delta.x,delta.y,GetClientRectHook(),NULL,0,0,flags );
		}

		OnViewOriginChange(m_viewOrigin - old);
	}
}

CPoint CListControlImpl::ClipViewOrigin(CPoint p_origin) const {
	return ClipPointToRect(p_origin,GetValidViewOriginArea());
}
void CListControlImpl::MoveViewOrigin(CPoint p_target) {
	PrepLayoutCache(p_target);
	MoveViewOriginNoClip(ClipViewOrigin(p_target));
}

#ifndef SPI_GETWHEELSCROLLCHARS
#define SPI_GETWHEELSCROLLCHARS   0x006C
#endif
int CListControlImpl::HandleWheel(int & p_accum,int p_delta, bool bHoriz) {
	if ( m_suppressMouseWheel ) return 0;
	UINT scrollLines = 1;
	SystemParametersInfo(bHoriz ? SPI_GETWHEELSCROLLCHARS : SPI_GETWHEELSCROLLLINES,0,&scrollLines,0);
	if (scrollLines == ~0) {
		p_accum = 0;
		int rv = -pfc::sgn_t(p_delta);
		CRect client = GetClientRectHook();
		if (bHoriz) rv *= client.Width();
		else rv *= client.Height();
		return rv;
	}

	const int itemHeight = GetItemHeight();
	const int extraScale = 10000;

	p_accum += p_delta * extraScale;
	if ((int)scrollLines < 1) scrollLines = 1;
	int multiplier = (WHEEL_DELTA * extraScale) / (scrollLines * itemHeight);
	if (multiplier<1) multiplier = 1;

	int delta = pfc::rint32( (double) p_accum / (double) multiplier );
	p_accum -= delta * multiplier;
	return -delta;

	/*
	if (p_accum<=-multiplier || p_accum>=multiplier) {
		int direction;
		int ov = p_accum;
		if (ov<0) {
			direction = -1;
			ov = -ov;
			p_accum = - ((-p_accum)%multiplier);
		} else {
			p_accum %= multiplier;
			direction = 1;
		}

		return  - (direction * (ov + multiplier - 1) ) / multiplier;
	} else {
		return 0;
	}
	*/
}

LRESULT CListControlImpl::OnVWheel(UINT,WPARAM p_wp,LPARAM,BOOL&) {
	const CRect client = GetClientRectHook(), view = this->GetViewAreaRectAbs();
	int deltaPixels = HandleWheel(m_wheelAccumY,(short)HIWORD(p_wp), false);

	const bool canVScroll = client.Height() < view.Height();
	const bool canHScroll = client.Width() < view.Width();
	
	CPoint ptDelta;
	if ( canVScroll && canHScroll && GetHotkeyModifierFlags() == MOD_SHIFT) {
		ptDelta = CPoint(deltaPixels, 0); // default to horizontal scroll if shift is pressed
	} else if (canVScroll) {
		ptDelta = CPoint(0,deltaPixels);
	} else if (canHScroll) {
		ptDelta = CPoint(deltaPixels,0);
	}

	if ( ptDelta != CPoint(0,0) ) {
		MoveViewOriginDelta(ptDelta);
	}
	return 0;
}
LRESULT CListControlImpl::OnHWheel(UINT,WPARAM p_wp,LPARAM,BOOL&) {
	// const CRect client = GetClientRectHook();
	int deltaPixels = HandleWheel(m_wheelAccumX,(short)HIWORD(p_wp), true);
	MoveViewOriginDelta(CPoint(-deltaPixels,0));
	return 0;
}

// WM_VSCROLL special fix
// We must expect SCROLLINFO to go out of sync with layout, due to group partitioning happening as the user scrolls
// SetScrollInfo() is apparently disregarded while the user is scrolling, causing nonsensical behavior if we live update it as we discover new groups
// When handling input, we must take the position as % of the set scrollbar range and map it to our coordinates - even though it is mappable directly if no groups etc are in use
LRESULT CListControlImpl::OnVScroll(UINT,WPARAM p_wp,LPARAM,BOOL&) {
	SCROLLINFO si = {};
	si.cbSize = sizeof(si);
	si.fMask = SIF_ALL;
	WIN32_OP_D(GetScrollInfo(SB_VERT, &si));
	int thumb = si.nTrackPos; // HIWORD(p_wp);
	auto bottom = GetViewAreaRectAbs().bottom;
	auto visible = GetVisibleHeight();

	if (si.nMax < si.nMin) return 0;
	double p = (double)(thumb - si.nMin) / (double)(si.nMax + 1 - si.nMin);
	thumb = pfc::rint32(p * bottom);
	int target = HandleScroll(LOWORD(p_wp), m_viewOrigin.y, visible, GetItemHeight(), bottom, thumb);

	Scroll_Debug_Print("OnVScroll thumb=", thumb, ", target=", target, ", bottom=", bottom, ", visible=", visible, ", p=", p);

	MoveViewOrigin(CPoint(m_viewOrigin.x, target));

	return 0;
}

// ====== Logitech scroll bug explanation ======
// With Logitech wheel hscroll, we must use WPARAM position, not GetScrollInfo() value.
// However this is wrong, we'll get nonsense if scroll range doesn't fit in 16-bit!
// As a workaround, we use GetScrollInfo() value for vscroll (good)
// and workaround Logitech bug by using WPARAM position with hscroll (practically impossible to overflow)
LRESULT CListControlImpl::OnHScroll(UINT,WPARAM p_wp,LPARAM,BOOL&) {
	int thumb = HIWORD(p_wp);
	const auto fullWidth = GetViewAreaWidth();
	if (fullWidth > INT16_MAX) { // Possible overflow or near-overflow? Drop Logitech stupidity mitigation
		thumb = GetScrollThumbPos(SB_HORZ);
	}
	int target = HandleScroll(LOWORD(p_wp), m_viewOrigin.x, GetVisibleRectAbs().Width(), GetItemHeight() /*fixme*/, fullWidth, thumb);
	Scroll_Debug_Print("OnHScroll thumb=", thumb, ", target=", target);
	MoveViewOrigin(CPoint(target,m_viewOrigin.y));
	return 0;
}

LRESULT CListControlImpl::OnGesture(UINT,WPARAM,LPARAM lParam,BOOL& bHandled) {
	if (!this->m_gestureAPI.IsAvailable()) {
		bHandled = FALSE;
		return 0;
	}
	HGESTUREINFO hGesture = (HGESTUREINFO) lParam;
	GESTUREINFO gestureInfo = {sizeof(gestureInfo)};
	if (m_gestureAPI.GetGestureInfo(hGesture, &gestureInfo)) {
		//console::formatter() << "WM_GESTURE " << pfc::format_hex( gestureInfo.dwFlags ) << " " << (int)gestureInfo.dwID << " X:" << gestureInfo.ptsLocation.x << " Y:" << gestureInfo.ptsLocation.y << " arg:" << (__int64) gestureInfo.ullArguments;
		CPoint pt( gestureInfo.ptsLocation.x, gestureInfo.ptsLocation.y );
		switch(gestureInfo.dwID) {
		case GID_BEGIN:
			m_gesturePoint = pt;
			break;
		case GID_END:
			break;
		case GID_PAN:
			MoveViewOriginDelta( this->m_gesturePoint - pt);
			m_gesturePoint = pt;
			break;
		}
	}

	m_gestureAPI.CloseGestureInfoHandle(hGesture);
	bHandled = TRUE;
	return 0;
}

LRESULT CListControlImpl::OnSize(UINT,WPARAM,LPARAM,BOOL&) {
	this->PrepLayoutCache(m_viewOrigin);
	OnSizeAsync_Trigger();
	RefreshSliders();
	return 0;
}


void CListControlImpl::RenderBackground( CDCHandle dc, CRect const & rc ) {
	PaintUtils::FillRectSimple(dc,rc,GetSysColorHook(colorBackground));
}

void CListControlImpl::PaintContent(CRect rcPaint, HDC dc) {
	CDCHandle renderDC(dc);

	CMemoryDC bufferDC(renderDC,rcPaint);
	renderDC = bufferDC;
	this->RenderBackground(renderDC, rcPaint);
		
	RenderRect(rcPaint, renderDC);
}

void CListControlImpl::OnPrintClient(HDC dc, UINT) {
	CRect rcClient; this->GetClientRect( rcClient );
	PaintContent( rcClient, dc );
}

void CListControlImpl::OnPaint(CDCHandle target) {
	auto toggle = pfc::autoToggle(m_paintInProgress, true);
	if (target) {
		CRect rcClient; this->GetClientRect(rcClient);
		PaintContent(rcClient, target);
	} else {
		CPaintDC paintDC(*this);
		PaintContent(paintDC.m_ps.rcPaint, paintDC.m_hDC);
	}
}

bool CListControlImpl::GetItemRange(const CRect & p_rect,t_size & p_base,t_size & p_count) const {
	return GetItemRangeAbs(this->RectClientToAbs(p_rect), p_base, p_count);
}



bool CListControlImpl::GetItemRangeAbsInclHeaders(const CRect & p_rect,t_size & p_base,t_size & p_count) const {
	CRect temp(p_rect);
	temp.bottom += this->GetGroupHeaderHeight();
	return GetItemRangeAbs(temp, p_base, p_count);
}

bool CListControlImpl::GetItemRangeAbs(const CRect & p_rect,t_size & p_base,t_size & p_count) const {
	const size_t count = GetItemCount();
	if (p_rect.right < 0 || p_rect.left >= GetItemWidth() || count == 0) return false;

	size_t top = IndexFromPointAbs(CPoint(0, p_rect.top));
	size_t bottom = IndexFromPointAbs(CPoint(0, p_rect.bottom));
	if (top == SIZE_MAX) return false;
	if (bottom > count-1) bottom = count - 1;
	p_base = top;
	p_count = bottom - top + 1;
	PFC_ASSERT(p_base + p_count <= count);
	return true;
}

void CListControlImpl::RenderRect(const CRect & p_rect,CDCHandle p_dc) {
	t_size base, count;
	if (GetItemRange(p_rect,base,count)) {
		for(t_size walk = 0; walk < count; ++walk) {
			size_t atItem = base + walk;
			if (m_groupHeaders.count(atItem) > 0) {
				CRect rcHeader, rcUpdate;
				if (GetGroupHeaderRectAbs2(atItem, rcHeader) ) {
					rcHeader = RectAbsToClient(rcHeader);
					if (rcUpdate.IntersectRect(rcHeader, p_rect)) {
						DCStateScope dcState(p_dc);
						if (p_dc.IntersectClipRect(rcUpdate) != NULLREGION) {
							try {
								RenderGroupHeader2(atItem, rcHeader, rcUpdate, p_dc);
							} catch (...) {
								PFC_ASSERT(!"Should not get here");
							}
						}
					}
				}
			}

			CRect rcUpdate, rcItem = GetItemRect(atItem);
			if (rcUpdate.IntersectRect(rcItem,p_rect)) {
				DCStateScope dcState(p_dc);
				if (p_dc.IntersectClipRect(rcUpdate) != NULLREGION) {
					try {
						RenderItem(atItem,rcItem,rcUpdate,p_dc);
					} catch(...) {
						PFC_ASSERT(!"Should not get here");
					}
				}
			}
		}

		if ( this->m_groupHeaders.size() > 0 ) {
			auto iter = m_groupHeaders.upper_bound(base);
			if (iter != m_groupHeaders.begin()) {
				--iter;
				while ( iter != m_groupHeaders.end() && *iter < base + count) {
					auto iter2 = iter; ++iter2;
					
					size_t begin = *iter;
					size_t end;
					if (iter2 == m_groupHeaders.end()) end = this->GetItemCount();
					else end = *iter2;

					CRect rc;
					rc.top = this->GetItemOffsetAbs(begin);
					rc.bottom = this->GetItemBottomOffsetAbs(end-1);
					rc.left = 0;
					rc.right = this->GetItemWidth();
					rc = this->RectAbsToClient(rc);
					CRect rcUpdate;
					if (rcUpdate.IntersectRect(rc, p_rect)) {
						DCStateScope dcState(p_dc);
						if (p_dc.IntersectClipRect(rcUpdate) != NULLREGION) {
							try {
								this->RenderGroupOverlay(begin, rc, rcUpdate, p_dc);
							} catch (...) {
								PFC_ASSERT(!"Should not get here");
							}
						}
					}


					iter = iter2;
				}
			}
		}
	}

	RenderOverlay2(p_rect,p_dc);
}

bool CListControlImpl::GetGroupOverlayRectAbs(size_t atItem, CRect& outRect) {
	auto iter = m_groupHeaders.upper_bound(atItem);
	if (iter == m_groupHeaders.begin()) return false;
	auto iter2 = iter; --iter;

	size_t begin = *iter;
	size_t end;
	if (iter2 == m_groupHeaders.end()) end = this->GetItemCount();
	else end = *iter2;

	CRect rc;

	rc.top = this->GetItemOffsetAbs(begin);
	rc.bottom = this->GetItemBottomOffsetAbs(end - 1);
	rc.left = 0;
	rc.right = this->GetItemWidth();

	outRect = rc;
	return true;
}

void CListControlImpl::MinGroupHeight2ChangedForGroup(groupID_t groupID, bool reloadWhole) {
	for (auto iter = m_groupHeaders.begin(); iter != m_groupHeaders.end(); ++iter) {
		if (groupID == GetItemGroup(*iter)) {
			this->MinGroupHeight2Changed(*iter, reloadWhole);
		}
	}
}

void CListControlImpl::UpdateGroupOverlayByID(groupID_t groupID, int xFrom, int xTo) {
	t_size base, count;
	if (GetItemRangeAbs(GetVisibleRectAbs(), base, count)) {
		bool on = false; // Have to walk whole range - there may be multiple groups with the same ID
		for (size_t walk = 0; walk < count; ++walk) {
			bool test = (groupID == GetItemGroup(base + walk));
			if (test && !on) {
				CRect rc;
				if (GetGroupOverlayRectAbs(base + walk, rc)) {
					if (xFrom < xTo) {
						rc.left = xFrom; rc.right = xTo;						
					}
					this->InvalidateRect(this->RectAbsToClient(rc));
				}
			}

			on = test;
		}
	}
}

CRect CListControlImpl::GetItemRect(t_size p_item) const {
	return this->RectAbsToClient(GetItemRectAbs(p_item));
}

bool CListControlImpl::GetGroupHeaderRect2(size_t atItem,CRect & p_rect) const {
	CRect temp;
	if (!GetGroupHeaderRectAbs2(atItem,temp)) return false;
	p_rect = RectAbsToClient(temp);
	return true;
}

size_t CListControlImpl::FindGroupBaseCached(size_t itemFor) const {
	auto iter = m_groupHeaders.upper_bound(itemFor);
	if (iter == m_groupHeaders.begin()) return 0;
	--iter;
	return *iter;
}

size_t CListControlImpl::FindGroupBase(size_t itemFor) const {
	return this->FindGroupBase(itemFor, this->GetItemGroup(itemFor));
}

size_t CListControlImpl::FindGroupBase(size_t itemFor, groupID_t id) const {
	size_t walk = itemFor;
	while (walk > 0) {
		size_t prev = walk - 1;
		if (this->GetItemGroup(prev) != id) break;
		walk = prev;
	}
	return walk;
}

bool CListControlImpl::PrepLayoutCache(CPoint& ptOrigin, size_t indexLo, size_t indexHi) {
	const size_t count = GetItemCount();
	if (count == 0) return false;
#if PrepLayoutCache_Debug
	PFC_DEBUGLOG << "PrepLayoutCache entry";
	PFC_DEBUGLOG << "PrepLayoutCache: count=" << count << " knownGroups=" << this->m_groupHeaders.size();
	PFC_DEBUGLOG << "PrepLayoutCache: indexLo=" << pfc::format_index(indexLo) << " indexHi=" << pfc::format_index(indexHi);
#endif
	const int clientHeight = pfc::max_t<int>(this->GetClientRectHook().Height(), 100);

	// Always walk 2*clientHeight, with area above and below
	int yMax = -1, yBase = 0;
	size_t baseItem = 0, endItem = SIZE_MAX;

	if (!m_greedyGroupLayout) {
		if (indexLo == SIZE_MAX) {
			yBase = pfc::max_t<int>(ptOrigin.y - clientHeight / 2, 0);
			yMax = yBase + clientHeight * 2;
			baseItem = pfc::min_t<size_t>(this->IndexFromPointAbs(yBase), count - 1);
		} else {
			auto itemHeight = GetItemHeight();
			size_t extraItems = (size_t)(clientHeight / itemHeight);
#if PrepLayoutCache_Debug
			PFC_DEBUGLOG << "PrepLayoutCache: clientHeight=" << clientHeight << " itemHeight=" << itemHeight << " extraItems=" << extraItems;
#endif
			if (indexLo < extraItems) baseItem = 0;
			else baseItem = indexLo - extraItems;

			if (indexHi == SIZE_MAX) {
				endItem = baseItem + extraItems;
			} else {
				endItem = indexHi + extraItems;
			}
			if (endItem > count) endItem = count;

#if PrepLayoutCache_Debug
			PFC_DEBUGLOG << "PrepLayoutCache: baseItem=" << baseItem << " endItem=" << endItem;
#endif
		}
	}




	size_t item = baseItem;
	{
		const auto group = this->GetItemGroup(baseItem);
		if (group != 0) {
			size_t hdr = this->FindGroupBase(baseItem, group);
			if (hdr < baseItem) {
				item = hdr;
			}
		}
	}

#if PrepLayoutCache_Debug
	if (yMax != -1) {
		PFC_DEBUGLOG << "PrepLayoutCache: yBase=" << yBase << " yMax=" << yMax;
	}
	if (indexLo != SIZE_MAX) {
		pfc::string_formatter msg;
		msg << "PrepLayoutCache: indexLo=" << indexLo;
		if (indexHi != SIZE_MAX) {
			msg << " indexHi=" << indexHi;
		}
		pfc::outputDebugLine(msg);
	}
	PFC_DEBUGLOG << "PrepLayoutCache: baseItem=" << baseItem;
#endif
	
	size_t anchorIdx = m_greedyGroupLayout ? SIZE_MAX : this->IndexFromPointAbs(ptOrigin.y);
	int anchorDelta = 0;
	bool anchorIsFirstInGroup = IsItemFirstInGroupCached(anchorIdx);
	if (anchorIdx != SIZE_MAX) {
		anchorDelta = ptOrigin.y - GetItemOffsetAbs(anchorIdx);
	}

#if PrepLayoutCache_Debug
	PFC_DEBUGLOG << "PrepLayoutCache: anchorIdx=" << pfc::format_index(anchorIdx) << " anchorDelta=" << anchorDelta << " anchorIsFirstInGroup=" << anchorIsFirstInGroup;
#endif

	bool bChanged = false;
	int gh = -1;
	int ih = -1;
	int yWalk = yBase;
	groupID_t prevGroup = 0;
	if (item > 1) prevGroup = this->GetItemGroup(item - 1);
	for (; item < count; ++item) {
		int yDelta = 0;
		auto group = this->GetItemGroup(item);
		if (group != prevGroup) {
			if (m_groupHeaders.insert(item).second) bChanged = true;
			if (gh < 0) gh = GetGroupHeaderHeight();
			yDelta += gh;
		} else {
			if (m_groupHeaders.erase(item) > 0) bChanged = true;
		}
		prevGroup = group;

		auto iter = m_varItemHeights.find(item);
		int varHeight = this->GetItemHeight2(item);
		if (varHeight < 0) {
			if (iter != m_varItemHeights.end()) {
				m_varItemHeights.erase(iter);
				bChanged = true;
			}
			if (ih < 0) ih = this->GetItemHeight();
			yDelta += ih;
		} else {
			if (iter == m_varItemHeights.end()) {
				m_varItemHeights[item] = varHeight;
				bChanged = true;
			} else if ( iter->second != varHeight ) {
				iter->second = varHeight;
				bChanged = true;
			}
		
			yDelta += varHeight;
		}

		if (item >= endItem) {
			break;
		}
		if (item >= baseItem && yMax != -1) {
			yWalk += yDelta;
			if (yWalk > yMax) break;
		}
	}

#if PrepLayoutCache_Debug
	PFC_DEBUGLOG << "PrepLayoutCache: bChanged=" << bChanged << " knownGroups=" << m_groupHeaders.size() << " knownVarHeights=" << m_varItemHeights.size();
#endif

	if (bChanged) {
		if (anchorIdx != SIZE_MAX) {
			int fix = GetItemOffsetAbs(anchorIdx) + anchorDelta;

			// View would begin exactly with an item that became a first item in a group?
			if (anchorDelta == 0 && !anchorIsFirstInGroup && IsItemFirstInGroupCached(anchorIdx)) {
				if (gh < 0) gh = GetGroupHeaderHeight();
				fix -= gh;
			}

#if PrepLayoutCache_Debug
			PFC_DEBUGLOG << "PrepLayoutCache: fixing origin: " << ptOrigin.y << " to " << fix;
#endif

			ptOrigin.y = fix;
			if (&ptOrigin != &m_viewOrigin && m_hWnd != NULL) {
#if PrepLayoutCache_Debug
				PFC_DEBUGLOG << "PrepLayoutCache: invalidating view";
#endif
				Invalidate();
			}
		}
	}

	if ( bChanged ) {
		// DO NOT update sliders from here, causes mayhem, SetScrollInfo() in mid-scroll is not really handled
		// this->RefreshSliders();
	}
	return bChanged;
}

int CListControlImpl::GetViewAreaHeight() const {
	auto ret = GetItemOffsetAbs(GetItemCount());
	Scroll_Debug_Print("GetViewAreaHeight: " , ret);
	return ret;
}

int CListControlImpl::GetItemBottomOffsetAbs(size_t item) const {
	return GetItemOffsetAbs(item) + GetItemHeightCached(item);
}

int CListControlImpl::GetItemOffsetAbs2(size_t base, size_t item) const {
	// Also valid with item == GetItemCount()
	size_t varcount = 0;
	int acc = 0;
	const bool baseValid = (base != SIZE_MAX);
	const size_t itemDelta = baseValid ? item - base : item;
	for (auto iter = (baseValid ? m_varItemHeights.lower_bound(base) : m_varItemHeights.begin()); iter != m_varItemHeights.end(); ++iter){
		if (iter->first >= item) break;
		if (iter->second > 0) acc += iter->second;
		++varcount;
	}
	if (varcount < itemDelta) {
		acc += GetItemHeight() * (int)(itemDelta - varcount);
	}

	int gh = -1;
	for (auto iter = (baseValid ? m_groupHeaders.upper_bound(base) : m_groupHeaders.begin()); iter != m_groupHeaders.end(); ++iter){
		if (*iter > item) break;
		if (gh < 0) gh = GetGroupHeaderHeight();
		acc += gh;
	}

	return acc;
}

int CListControlImpl::GetItemOffsetAbs(size_t item) const {
	// Also valid with item == GetItemCount()
	return GetItemOffsetAbs2(SIZE_MAX, item);
}

int CListControlImpl::GetItemContentHeightCached(size_t item) const {
	auto iter = m_varItemHeights.find(item);
	if (iter == m_varItemHeights.end()) return GetItemHeight();
	else return this->GetItemHeight2Content( item, iter->second );
}

int CListControlImpl::GetItemHeightCached(size_t item) const {
	auto iter = m_varItemHeights.find(item);
	if (iter == m_varItemHeights.end()) return GetItemHeight();
	else return iter->second;
}

CRect CListControlImpl::GetItemRectAbs(t_size p_item) const {
	PFC_ASSERT(p_item < GetItemCount());
	// const int normalHeight = GetItemHeight();
	CRect rcItem;
	rcItem.top = GetItemOffsetAbs(p_item);
	rcItem.bottom = rcItem.top + GetItemContentHeightCached(p_item);
	rcItem.left = 0;
	rcItem.right = rcItem.left + GetItemWidth();
	return rcItem;
}

bool CListControlImpl::GetGroupHeaderRectAbs2(size_t atItem,CRect & p_rect) const {

	if (m_groupHeaders.count(atItem) == 0) return false;

	p_rect.bottom = GetItemOffsetAbs(atItem);
	p_rect.top = p_rect.bottom - GetGroupHeaderHeight();
	p_rect.left = 0;
	p_rect.right = GetItemWidth();
	return true;
}

CRect CListControlImpl::GetViewAreaRectAbs() const {
	return CRect(0,0,GetViewAreaWidth(),GetViewAreaHeight());
}

CRect CListControlImpl::GetViewAreaRect() const {
	CRect rc = GetViewAreaRectAbs();
	rc.OffsetRect( - GetViewOffset() );
	CRect ret; ret.IntersectRect(rc,GetClientRectHook());
	return ret;
}

void CListControlImpl::UpdateGroupHeader2(size_t atItem) {
	CRect rect;
	if (GetGroupHeaderRect2(atItem,rect)) {
		InvalidateRect(rect);
	}
}
static void AddUpdateRect(HRGN p_rgn,CRect const & p_rect) {
	CRgn temp; temp.CreateRectRgnIndirect(p_rect);
	CRgnHandle(p_rgn).CombineRgn(temp,RGN_OR);
}

void CListControlImpl::OnItemsReordered( const size_t * order, size_t count ) {
	PFC_ASSERT(count == GetItemCount()); (void)count;
	ReloadItems( pfc::bit_array_order_changed(order) );
}
void CListControlImpl::UpdateItems(const pfc::bit_array & p_mask) {
	t_size base,count;
	if (GetItemRangeAbs(GetVisibleRectAbs(),base,count)) {
		const t_size max = base+count;
		CRgn updateRgn; updateRgn.CreateRectRgn(0,0,0,0);
		bool found = false;
		for(t_size walk = p_mask.find_first(true,base,max); walk < max; walk = p_mask.find_next(true,walk,max)) {
			found = true;
			AddUpdateRect(updateRgn,GetItemRect(walk));
		}
		if (found) {
			InvalidateRgn(updateRgn);
		}
	}
}

std::pair<size_t, size_t> CListControlImpl::GetVisibleRange() const {
	const size_t total = GetItemCount();
	CRect rcVisible = this->GetVisibleRectAbs();
	size_t lo = this->IndexFromPointAbs(rcVisible.top);
	PFC_ASSERT(lo != SIZE_MAX);
	if (lo == SIZE_MAX) lo = 0; // should not happen
	size_t hi = this->IndexFromPointAbs(rcVisible.bottom);
	if (hi < total) ++hi;
	else hi = total;
	return { lo, hi };
}

bool CListControlImpl::IsItemVisible(size_t which) const {
	CRect rcVisible = this->GetVisibleRectAbs();
	CRect rcItem = this->GetItemRectAbs(which);
	return rcItem.top >= rcVisible.top && rcItem.bottom <= rcVisible.bottom;
}

void CListControlImpl::UpdateItemsAndHeaders(const pfc::bit_array & p_mask) {
	t_size base,count;
	groupID_t groupWalk = 0;
	if (GetItemRangeAbsInclHeaders(GetVisibleRectAbs(),base,count)) {
		const t_size max = base+count;
		CRgn updateRgn; updateRgn.CreateRectRgn(0,0,0,0);
		bool found = false;
		for(t_size walk = p_mask.find_first(true,base,max); walk < max; walk = p_mask.find_next(true,walk,max)) {
			found = true;
			const groupID_t groupId = GetItemGroup(walk);
			if (groupId != groupWalk) {
				CRect rect;
				if (GetGroupHeaderRect2(walk,rect)) {
					AddUpdateRect(updateRgn,rect);
				}
				groupWalk = groupId;
			}
			AddUpdateRect(updateRgn,GetItemRect(walk));
		}
		if (found) {
			InvalidateRgn(updateRgn);
		}
	}
}


CRect CListControlImpl::GetValidViewOriginArea() const {
	const CRect rcView = GetViewAreaRectAbs();
	const CRect rcClient = GetClientRectHook();
	CRect rcArea = rcView;
	rcArea.right -= pfc::min_t(rcView.Width(),rcClient.Width());
	rcArea.bottom -= pfc::min_t(rcView.Height(),rcClient.Height());
	return rcArea;
}

void CListControlImpl::OnViewAreaChanged(CPoint p_originOverride) {
	const CPoint oldViewOrigin = m_viewOrigin;

	PrepLayoutCache(p_originOverride);

	m_viewOrigin = ClipPointToRect(p_originOverride,GetValidViewOriginArea());

	if (m_viewOrigin != p_originOverride) {
		// Did clip from the requested?
		PrepLayoutCache(m_viewOrigin);
	}
#if PrepLayoutCache_Debug
	PFC_DEBUGLOG << "OnViewAreaChanged: m_viewOrigin=" << m_viewOrigin.x << "," << m_viewOrigin.y;
#endif

	RefreshSliders();

	Invalidate();
	
	if (oldViewOrigin != m_viewOrigin) {
		OnViewOriginChange(m_viewOrigin - oldViewOrigin);
	}
}

size_t CListControlImpl::IndexFromPointAbs(CPoint pt) const {
	if (pt.x < 0 || pt.x >= GetItemWidth()) return SIZE_MAX;
	return IndexFromPointAbs(pt.y);
}

size_t CListControlImpl::IndexFromPointAbs(int ptY) const {
	const size_t count = GetItemCount();
	if (count == 0) return SIZE_MAX;
	
	class wrapper {
	public:
		wrapper(const CListControlImpl & o) : owner(o) {}
		int operator[] (size_t idx) const {
			// Return LAST line of this item
			return owner.GetItemBottomOffsetAbs(idx)-1;
		}
		const CListControlImpl & owner;
	};

	wrapper w(*this);
	size_t result = SIZE_MAX;
	pfc::binarySearch<>::run(w, 0, count, ptY, result);
	PFC_ASSERT(result != SIZE_MAX);
	return result;
}

bool CListControlImpl::ItemFromPointAbs(CPoint const & p_pt,t_size & p_item) const {
	size_t idx = IndexFromPointAbs(p_pt);
	if (idx >= GetItemCount()) return false;
	CRect rc = this->GetItemRectAbs(idx);
	if (!rc.PtInRect(p_pt)) return false;
	p_item = idx;
	return true;
}

size_t CListControlImpl::ItemFromPointAbs(CPoint const& p_pt) const {
	size_t ret = SIZE_MAX;
	ItemFromPointAbs(p_pt, ret);
	return ret;
}

bool CListControlImpl::GroupHeaderFromPointAbs2(CPoint const & p_pt,size_t & atItem) const {
	size_t idx = IndexFromPointAbs(p_pt);
	if (idx == SIZE_MAX) return false;
	CRect rc;
	if (!this->GetGroupHeaderRectAbs2(idx, rc)) return false;
	if (!rc.PtInRect(p_pt)) return false;
	atItem = idx;
	return true;
}

void CListControlImpl::OnThemeChanged() {
	m_themeCache.remove_all();
}

CTheme & CListControlImpl::themeFor(const char * what) {
	bool bNew;
	auto & ret = this->m_themeCache.find_or_add_ex( what, bNew );
	if (bNew) ret.OpenThemeData(*this, pfc::stringcvt::string_wide_from_utf8(what));
	return ret;
}

void CListControlImpl::SetDarkMode(bool v) {
	if (m_darkMode != v) {
		m_darkMode = v;
		RefreshDarkMode();
	}
}

void CListControlImpl::RefreshDarkMode() {
	if (m_hWnd != NULL) {
		Invalidate();

		// GOD DAMNIT: Should use ItemsView, but only Explorer fixes scrollbars
		DarkMode::ApplyDarkThemeCtrl(m_hWnd, m_darkMode, L"Explorer");
	}
}

LRESULT CListControlImpl::OnCreatePassThru(UINT,WPARAM,LPARAM,BOOL& bHandled) {

	RefreshDarkMode();
	
	
	OnViewAreaChanged();

	if (m_gestureAPI.IsAvailable()) {
		GESTURECONFIG config = {GID_PAN, GC_PAN_WITH_SINGLE_FINGER_VERTICALLY|GC_PAN_WITH_INERTIA, GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY | GC_PAN_WITH_GUTTER};
		m_gestureAPI.SetGestureConfig( *this, 0, 1, &config, sizeof(GESTURECONFIG));
	}

	bHandled = FALSE;
	return 0;
}
bool CListControlImpl::IsSameItemOrHeaderAbs(const CPoint & p_point1, const CPoint & p_point2) const {
	t_size item1, item2;
	if (ItemFromPointAbs(p_point1, item1)) {
		if (ItemFromPointAbs(p_point2,item2)) {
			return item1 == item2;
		} else {
			return false;
		}
	}
	if (GroupHeaderFromPointAbs2(p_point1, item1)) {
		if (GroupHeaderFromPointAbs2(p_point2, item2)) {
			return item1 == item2;
		} else {
			return false;
		}
	}
	return false;
}

void CListControlImpl::OnSizeAsync_Trigger() {
	if (!m_sizeAsyncPending) {
		if (PostMessage(MSG_SIZE_ASYNC,0,0)) {
			m_sizeAsyncPending = true;
		} else {
			PFC_ASSERT(!"Shouldn't get here!");
			//should not happen
			ListHandleResize();
		}
	}
}

void CListControlImpl::ListHandleResize() {
	MoveViewOriginDelta(CPoint(0,0));
	m_sizeAsyncPending = false;
}

void CListControlImpl::AddGroupHeaderToUpdateRgn2(HRGN p_rgn, size_t atItem) const {
	CRect rcHeader;
	if (GetGroupHeaderRect2(atItem,rcHeader)) AddUpdateRect(p_rgn,rcHeader);
}
void CListControlImpl::AddItemToUpdateRgn(HRGN p_rgn, t_size p_index) const {
	if (p_index < this->GetItemCount()) {
		AddUpdateRect(p_rgn,GetItemRect(p_index));
	}
}

COLORREF CListControlImpl::GetSysColorHook(int colorIndex) const {
	if (m_darkMode) {
		return DarkMode::GetSysColor(colorIndex);
	} else {
		return GetSysColor(colorIndex);
	}
}

BOOL CListControlImpl::OnEraseBkgnd(CDCHandle dc) {
	
	if (paintInProgress()) return FALSE;

	CRect rcClient; WIN32_OP_D(GetClientRect(rcClient)); // SPECIAL CASE: No GetClientRectHook() here, fill physical client area, not logical
	PaintUtils::FillRectSimple(dc,rcClient,this->GetSysColorHook(COLOR_WINDOW));

	return TRUE;
}

t_size CListControlImpl::InsertIndexFromPointEx(const CPoint & pt, bool & bInside) const {
	bInside = false;
	int y_abs = pt.y + GetViewOffset().y;
	
	if (y_abs >= GetViewAreaHeight()) {
		return GetItemCount();
	}
	size_t itemIdx = IndexFromPointAbs(y_abs);
	if (itemIdx == SIZE_MAX) return SIZE_MAX;

	{
		CRect rc;
		if (!this->GetGroupHeaderRectAbs2(itemIdx, rc)) {
			if (y_abs >= rc.top && y_abs <= rc.bottom) {
				bInside = false;
				return itemIdx;
			}
		}
	}
	if (itemIdx != SIZE_MAX) {
		const CRect rc = GetItemRectAbs(itemIdx);
		if (y_abs > rc.top + MulDiv(rc.Height(), 2, 3)) itemIdx++;
		else if (y_abs >= rc.top + MulDiv(rc.Height(), 1, 3)) bInside = true;
		return itemIdx;
	}
	return SIZE_MAX;
}

t_size CListControlImpl::InsertIndexFromPoint(const CPoint & pt) const {
	bool dummy; return InsertIndexFromPointEx(pt,dummy);
}

COLORREF CListControlImpl::BlendGridColor( COLORREF bk ) {
	return BlendGridColor( bk, PaintUtils::DetermineTextColor( bk ) );
}

COLORREF CListControlImpl::BlendGridColor( COLORREF bk, COLORREF tx ) {
	return PaintUtils::BlendColor(bk, tx, 10);
}

COLORREF CListControlImpl::GridColor() {
	return BlendGridColor( GetSysColorHook(colorBackground), GetSysColorHook(colorText) );
}

void CListControlImpl::RenderItemBackground(CDCHandle p_dc,const CRect & p_itemRect,size_t p_item, uint32_t bkColor) {
	switch( this->m_rowStyle ) {
	case rowStylePlaylistDelimited:
		PaintUtils::RenderItemBackground(p_dc,p_itemRect,p_item,bkColor);
		{
			auto blend = BlendGridColor(bkColor);
			CDCPen pen(p_dc, blend);
			SelectObjectScope scope(p_dc, pen);

			p_dc.MoveTo( p_itemRect.right-1, p_itemRect.top );
			p_dc.LineTo( p_itemRect.right-1, p_itemRect.bottom );
		}
		break;
	case rowStylePlaylist:
		PaintUtils::RenderItemBackground(p_dc,p_itemRect,p_item,bkColor);
		break;
	case rowStyleGrid:
		PaintUtils::FillRectSimple(p_dc, p_itemRect, bkColor );
		{
			auto blend = BlendGridColor(bkColor);
			CDCBrush brush(p_dc, blend);
			p_dc.FrameRect(&p_itemRect, brush);

		}
		break;
	case rowStyleFlat:
		PaintUtils::FillRectSimple(p_dc, p_itemRect, bkColor );
		break;
	}
}

void CListControlImpl::RenderGroupHeaderBackground(CDCHandle p_dc,const CRect & p_headerRect,int p_group) {
	(void)p_group;
	const t_uint32 bkColor = GetSysColorHook(colorBackground);
	size_t pretendIndex = 0;
	switch( this->m_rowStyle ) {
	default:
		PaintUtils::FillRectSimple( p_dc, p_headerRect, bkColor );
		break;
	case rowStylePlaylistDelimited:
	case rowStylePlaylist:
		PaintUtils::RenderItemBackground(p_dc,p_headerRect,pretendIndex,bkColor);
		break;
	}
}

void CListControlImpl::RenderItem(t_size p_item,const CRect & p_itemRect,const CRect & p_updateRect,CDCHandle p_dc) {
	this->RenderItemBackground(p_dc, p_itemRect, p_item, GetSysColorHook(colorBackground) );

	DCStateScope backup(p_dc);
	p_dc.SetBkMode(TRANSPARENT);
	p_dc.SetBkColor(GetSysColorHook(colorBackground));
	p_dc.SetTextColor(GetSysColorHook(colorText));

	RenderItemText(p_item,p_itemRect,p_updateRect,p_dc, true);
}

void CListControlImpl::RenderGroupHeader2(size_t baseItem,const CRect & p_headerRect,const CRect & p_updateRect,CDCHandle p_dc) {
	this->RenderGroupHeaderBackground(p_dc, p_headerRect, 0 );

	DCStateScope backup(p_dc);
	p_dc.SetBkMode(TRANSPARENT);
	p_dc.SetBkColor(GetSysColorHook(colorBackground));
	p_dc.SetTextColor(GetSysColorHook(colorHighlight));

	RenderGroupHeaderText2(baseItem,p_headerRect,p_updateRect,p_dc);
}


CListControlFontOps::CListControlFontOps() : m_font((HFONT)::GetStockObject(DEFAULT_GUI_FONT)), m_itemHeight(), m_groupHeaderHeight() {
	UpdateGroupHeaderFont();
	CalculateHeights();
}

void CListControlFontOps::UpdateGroupHeaderFont() {
	try {
		m_groupHeaderFont = NULL;
		LOGFONT lf = {};
		WIN32_OP_D( m_font.GetLogFont(lf) );
		lf.lfHeight = pfc::rint32( (double) lf.lfHeight * GroupHeaderFontScale() );
		lf.lfWeight = GroupHeaderFontWeight(lf.lfWeight);
		WIN32_OP_D( m_groupHeaderFont.CreateFontIndirect(&lf) != NULL );
	} catch(std::exception const & e) {
		(void) e;
		// console::print(e.what());
		m_groupHeaderFont = (HFONT)::GetStockObject(DEFAULT_GUI_FONT);
	}
}

void CListControlFontOps::CalculateHeights() {
	const t_uint32 spacing = MulDiv(4, m_dpi.cy, 96);
	m_itemHeight = GetFontHeight( m_font ) + spacing;
	m_groupHeaderHeight = GetFontHeight( m_groupHeaderFont ) + spacing;
}

void CListControlFontOps::SetFont(HFONT font,bool bUpdateView) {
	m_font = font;
	UpdateGroupHeaderFont(); CalculateHeights();
	OnSetFont(bUpdateView);
	if (bUpdateView && m_hWnd != NULL) OnViewAreaChanged();
	
}

LRESULT CListControlFontOps::OnSetFont(UINT,WPARAM wp,LPARAM,BOOL&) {
	SetFont((HFONT)wp);
	return 0;
}

LRESULT CListControlFontOps::OnGetFont(UINT,WPARAM,LPARAM,BOOL&) {
	return (LRESULT)(HFONT)m_font;
}

LRESULT CListControlImpl::OnGetDlgCode(UINT, WPARAM wp, LPARAM) {
	switch(wp) {
	case VK_RETURN:
		return m_dlgWantEnter ? DLGC_WANTMESSAGE : 0;
	default:
		SetMsgHandled(FALSE);
		return 0;
	}
}

HWND CListControlImpl::CreateInDialog(CWindow wndDialog, UINT replaceControlID, CWindow lstReplace) {
	PFC_ASSERT(lstReplace != NULL);
	auto status = lstReplace.SendMessage(WM_GETDLGCODE, VK_RETURN);
	m_dlgWantEnter = (status & DLGC_WANTMESSAGE);
	CRect rc;
	CWindow wndPrev = wndDialog.GetNextDlgTabItem(lstReplace, TRUE);
	WIN32_OP_D(lstReplace.GetWindowRect(&rc));
	WIN32_OP_D(wndDialog.ScreenToClient(rc));
	WIN32_OP_D(lstReplace.DestroyWindow());
	WIN32_OP_D(this->Create(wndDialog, &rc, 0, 0, WS_EX_STATICEDGE, replaceControlID));
	if (wndPrev != NULL) this->SetWindowPos(wndPrev, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
	// this->BringWindowToTop();
	this->SetFont(wndDialog.GetFont());
	return m_hWnd;
}

HWND CListControlImpl::CreateInDialog(CWindow wndDialog, UINT replaceControlID ) {
	return this->CreateInDialog(wndDialog, replaceControlID, wndDialog.GetDlgItem(replaceControlID));
}


void CListControlImpl::defer(std::function<void() > f) {
	m_deferred.push_back( f );
	if (!m_defferredMsgPending) {
		if ( PostMessage(MSG_EXEC_DEFERRED) ) m_defferredMsgPending = true;
	}
}

LRESULT CListControlImpl::OnExecDeferred(UINT, WPARAM, LPARAM) {
	
	for ( ;; ) { 
		auto i = m_deferred.begin();
		if ( i == m_deferred.end() ) break;
		auto op = std::move(*i);
		m_deferred.erase(i); // erase first, execute later - avoid erratic behavior if op alters the list
		op();
	}

	m_defferredMsgPending = false;
	return 0;
}

// ========================================================================================
// Mouse wheel vs drag&drop hacks
// Install MouseHookProc for the duration of DoDragDrop and handle the input from there
// ========================================================================================
static HHOOK g_hook = NULL;
static CListControlImpl * g_dragDropInstance = nullptr;
LRESULT CALLBACK CListControlImpl::MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam) {
	if (nCode == HC_ACTION && g_dragDropInstance != nullptr) {
		switch (wParam) {
		case WM_MOUSEWHEEL:
		case WM_MOUSEHWHEEL:
			g_dragDropInstance->MouseWheelFromHook((UINT)wParam, lParam);
			break;
		}
	}
	return CallNextHookEx(g_hook, nCode, wParam, lParam);
}

bool CListControlImpl::MouseWheelFromHook(UINT msg, LPARAM data) {
	MOUSEHOOKSTRUCTEX const * mhs = reinterpret_cast<MOUSEHOOKSTRUCTEX const *> ( data ); 
	if ( ::WindowFromPoint(mhs->pt) != m_hWnd ) return false;
	LRESULT dummyResult = 0;
	WPARAM wp = mhs->mouseData;
	LPARAM lp = MAKELPARAM( mhs->pt.x, mhs->pt.y );
	// If we get here, m_suppressMouseWheel should be true per our DoDragDrop()
	pfc::vartoggle_t<bool> scope(m_suppressMouseWheel, false);
	this->ProcessWindowMessage( m_hWnd, msg, wp, lp, dummyResult );
	return true;
}

HRESULT CListControlImpl::DoDragDrop(LPDATAOBJECT pDataObj, LPDROPSOURCE pDropSource, DWORD dwOKEffects, LPDWORD pdwEffect) {
	HRESULT ret = E_FAIL;
	// Should not get here with non null g_dragDropInstance - means we have a recursive call
	PFC_ASSERT(g_dragDropInstance == nullptr);
	if ( g_dragDropInstance == nullptr ) {
		// futureproofing: kill mouse wheel message processing if we get them delivered the regular way while this is in progress
		pfc::vartoggle_t<bool> scope(m_suppressMouseWheel, true);
		g_dragDropInstance = this;
		g_hook = SetWindowsHookEx(WH_MOUSE, MouseHookProc, NULL, GetCurrentThreadId());
		try {
			ret = ::DoDragDrop(pDataObj, pDropSource, dwOKEffects, pdwEffect);
		} catch (...) {
		}
		g_dragDropInstance = nullptr;
		UnhookWindowsHookEx(pfc::replace_null_t(g_hook));
	}
	return ret;
}


CPoint CListControlImpl::PointAbsToClient(CPoint pt) const {
	return pt - GetViewOffset();
}

CPoint CListControlImpl::PointClientToAbs(CPoint pt) const {
	return pt + GetViewOffset();
}

CRect CListControlImpl::RectAbsToClient(CRect rc) const {
	CRect ret;
#if 1
	ret = rc;
	ret.OffsetRect(-GetViewOffset());
#else
	ret.TopLeft() = PointAbsToClient(rc.TopLeft());
	ret.BottomRight() = PointAbsToClient(rc.BottomRight());
#endif
	return ret;
}

CRect CListControlImpl::RectClientToAbs(CRect rc) const {
	CRect ret;
#if 1
	ret = rc;
	ret.OffsetRect(GetViewOffset());
#else
	ret.TopLeft() = PointClientToAbs(rc.TopLeft());
	ret.BottomRight() = PointAbsToClient(rc.BottomRight());
#endif
	return ret;
}

size_t CListControlImpl::ItemFromPoint(CPoint const& pt) const {
	size_t ret = SIZE_MAX;
	if (!ItemFromPoint(pt, ret)) ret = SIZE_MAX;
	return ret;
}

bool CListControlImpl::ItemFromPoint(CPoint const & p_pt, t_size & p_item) const {
	return ItemFromPointAbs( PointClientToAbs( p_pt ), p_item);
}

void CListControlImpl::ReloadData() {
	this->m_varItemHeights.clear();
	this->m_groupHeaders.clear();
	OnViewAreaChanged(); 
}

void CListControlImpl::ReloadItems(pfc::bit_array const & mask) { 
	bool bReLayout = false;
	mask.for_each(true, 0, GetItemCount(), [this, &bReLayout] (size_t idx) {
		int hNew = this->GetItemHeight2(idx);
		int hOld = -1;
		{
			auto iter = m_varItemHeights.find(idx);
			if (iter != m_varItemHeights.end()) hOld = iter->second;
		}
		if (hNew != hOld) {
			m_varItemHeights[idx] = hNew;
			bReLayout = true;
		}
	});
	if (bReLayout) {
		OnViewAreaChanged();
	} else {
		UpdateItems(mask);
	}
	
}

void CListControlImpl::MinGroupHeight2Changed(size_t itemInGroup, bool reloadWhole) {
	size_t lo, hi;
	if (ResolveGroupRangeCached(itemInGroup, lo, hi)) {
		if (reloadWhole) {
			CRect rc;
			if (this->GetGroupOverlayRectAbs(itemInGroup, rc)) {
				this->InvalidateRect(this->RectAbsToClient(rc));
			}
			{
				pfc::bit_array_range range(lo, hi-lo);
				this->ReloadItems(range);
			}
		} else {
			this->ReloadItem(hi - 1);
		}
	}
}

bool CListControlImpl::IsItemFirstInGroupCached(size_t item) const {
	return m_groupHeaders.count(item) > 0;
}

bool CListControlImpl::IsItemFirstInGroup(size_t item) const {
	if (item == 0) return true;
	return GetItemGroup(item) != GetItemGroup(item - 1);
}
bool CListControlImpl::IsItemLastInGroup(size_t item) const {
	size_t next = item + 1;
	if (next >= GetItemCount()) return true;
	return GetItemGroup(item) != GetItemGroup(next);
}

int CListControlImpl::GetItemHeight2(size_t which) const {
	if (!IsItemLastInGroup(which)) return -1;

	const int minGroupHeight = this->GetMinGroupHeight2(which);
	if (minGroupHeight <= 0) return -1;

	const int heightNormal = this->GetItemHeight();

	const size_t base = FindGroupBase(which);
	
	const int groupHeightWithout = (which > base ? this->GetItemOffsetAbs2(base,which) : 0);
	
	const int minItemHeight = minGroupHeight - groupHeightWithout; // possibly negative

	if (minItemHeight > heightNormal) return minItemHeight;
	else return -1; // use normal	
}

void CListControlImpl::wndSetDarkMode(CWindow wndListControl, bool bDark) { 
	wndListControl.SendMessage(DarkMode::msgSetDarkMode(), bDark ? 1 : 0);
}

LRESULT CListControlImpl::OnSetDark(UINT, WPARAM wp, LPARAM) {
	switch (wp) {
	case 0:
		this->SetDarkMode(false);
		break;
	case 1:
		this->SetDarkMode(true);
		break;
	}
	return 1;
}

void CListControlImpl::OnItemRemoved(size_t which) {
	this->OnItemsRemoved(pfc::bit_array_one(which), GetItemCount() + 1);
}


UINT CListControlImpl::msgSetDarkMode() {
	return DarkMode::msgSetDarkMode();
}

void CListControlImpl::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) {
	(void)nRepCnt; (void)nFlags;
	switch (nChar) {
	case VK_LEFT:
		MoveViewOriginDelta(CPoint(-MulDiv(16, m_dpi.cx, 96), 0));
		break;
	case VK_RIGHT:
		MoveViewOriginDelta(CPoint(MulDiv(16, m_dpi.cx, 96), 0));
		break;
	default:
		SetMsgHandled(FALSE); break;
	}
}

void CListControlImpl::OnItemsInserted(size_t at, size_t count, bool bSelect) {
	size_t newCount = this->GetItemCount();
	this->OnItemsInsertedEx(pfc::bit_array_range(at, count, true), newCount - count, newCount, bSelect);
}