essence-os/desktop/list_view.cpp

3075 lines
105 KiB
C++

// This file is part of the Essence operating system.
// It is released under the terms of the MIT license -- see LICENSE.md.
// Written by: nakst.
// TODO RMB click/drag.
// TODO Consistent int64_t/intptr_t.
// TODO Drag and drop.
// TODO GetFirstIndex/GetLastIndex assume that every group is non-empty.
// TODO Sticking to top/bottom scroll when inserting/removing space.
// TODO Audit usage of MeasureItems -- it doesn't take into account the gap between items!
struct ListViewItemElement : EsElement {
uintptr_t index; // Index into the visible items array.
};
struct ListViewItem {
ListViewItemElement *element;
EsListViewIndex group;
int32_t size;
EsListViewIndex index;
uint8_t indent;
bool startAtSecondColumn;
bool isHeader, isFooter;
bool showSearchHighlight;
};
struct ListViewGroup {
// TODO Empty groups.
EsListViewIndex itemCount;
int64_t totalSize;
uint32_t flags;
bool initialised;
};
struct ListViewFixedString {
char *string;
size_t bytes;
};
struct ListViewFixedItem {
EsGeneric data;
uint32_t iconID;
};
struct ListViewFixedItemData {
union {
ListViewFixedString s;
double d;
int64_t i;
};
};
struct ListViewColumn {
char *title;
size_t titleBytes;
uint32_t id;
uint32_t flags;
double width;
Array<ListViewFixedItemData> items;
const EsListViewEnumString *enumStrings;
size_t enumStringCount;
};
typedef void (*ListViewSortFunction)(EsListViewIndex *, size_t, ListViewColumn *);
int ListViewProcessItemMessage(EsElement *element, EsMessage *message);
void ListViewSetSortAscending(EsMenu *menu, EsGeneric context);
void ListViewSetSortDescending(EsMenu *menu, EsGeneric context);
void ListViewPopulateActionCallback(EsElement *element, EsGeneric);
void ListViewEnsureVisibleActionCallback(EsElement *element, EsGeneric);
struct EsListView : EsElement {
ScrollPane scroll;
uint64_t totalItemCount;
uint64_t totalSize;
Array<ListViewGroup> groups;
Array<ListViewItem> visibleItems;
EsStyleID itemStyle, headerItemStyle, footerItemStyle;
int64_t fixedWidth, fixedHeight;
int64_t fixedHeaderSize, fixedFooterSize;
UIStyle *primaryCellStyle;
UIStyle *secondaryCellStyle;
UIStyle *selectedCellStyle;
bool hasFocusedItem;
EsListViewIndex focusedItemGroup;
EsListViewIndex focusedItemIndex;
bool hasAnchorItem;
EsListViewIndex anchorItemGroup;
EsListViewIndex anchorItemIndex;
bool hasScrollItem; // Used to preserve the scroll position when resizing a wrapped list view.
bool useScrollItem;
int64_t scrollItemOffset;
EsListViewIndex scrollItemGroup;
EsListViewIndex scrollItemIndex;
#define ENSURE_VISIBLE_ALIGN_TOP (1 << 0)
#define ENSURE_VISIBLE_ALIGN_CENTER (1 << 1)
#define ENSURE_VISIBLE_ALIGN_FOR_SCROLL_ITEM (1 << 2)
EsListViewIndex ensureVisibleGroupIndex;
EsListViewIndex ensureVisibleIndex;
uint8_t ensureVisibleFlags;
bool ensureVisibleQueued;
bool populateQueued;
// Valid only during Z-order messages.
Array<EsElement *> zOrderItems;
EsElement *selectionBox;
bool hasSelectionBoxAnchor;
int64_t selectionBoxAnchorX, selectionBoxAnchorY,
selectionBoxPositionX, selectionBoxPositionY;
bool firstLayout;
char searchBuffer[64];
size_t searchBufferBytes;
uint64_t searchBufferLastKeyTime;
char *emptyMessage;
size_t emptyMessageBytes;
EsElement *columnHeader;
Array<ListViewColumn> registeredColumns;
Array<uint32_t> activeColumns; // Indices into registeredColumns.
int columnResizingOriginalWidth;
int64_t totalColumnWidth;
EsTextbox *inlineTextbox;
EsListViewIndex inlineTextboxGroup;
EsListViewIndex inlineTextboxIndex;
int maximumItemsPerBand;
// Fixed item storage:
Array<ListViewFixedItem> fixedItems;
Array<EsListViewIndex> fixedItemIndices; // For sorting. Converts the actual list index into an index for fixedItems.
ptrdiff_t fixedItemSelection;
uint32_t fixedItemSortColumnID;
#define LIST_SORT_DIRECTION_ASCENDING (1)
#define LIST_SORT_DIRECTION_DESCENDING (2)
uint8_t fixedItemSortDirection;
inline EsRectangle GetListBounds() {
EsRectangle bounds = GetBounds();
if (columnHeader) {
bounds.t += columnHeader->style->preferredHeight;
}
return bounds;
}
inline void GetFirstIndex(EsMessage *message) {
EsAssert(message->iterateIndex.group < (EsListViewIndex) groups.Length()); // Invalid group index.
EsAssert(groups[message->iterateIndex.group].itemCount); // No items in the group.
message->iterateIndex.index = 0;
}
inline void GetLastIndex(EsMessage *message) {
EsAssert(message->iterateIndex.group < (EsListViewIndex) groups.Length()); // Invalid group index.
EsAssert(groups[message->iterateIndex.group].itemCount); // No items in the group.
message->iterateIndex.index = groups[message->iterateIndex.group].itemCount - 1;
}
inline bool IterateForwards(EsMessage *message) {
if (message->iterateIndex.index == groups[message->iterateIndex.group].itemCount - 1) {
if (message->iterateIndex.group == (EsListViewIndex) groups.Length() - 1) {
return false;
}
message->iterateIndex.group++;
message->iterateIndex.index = 0;
} else {
message->iterateIndex.index++;
}
return true;
}
inline bool IterateBackwards(EsMessage *message) {
if (message->iterateIndex.index == 0) {
if (message->iterateIndex.group == 0) {
return false;
}
message->iterateIndex.group--;
message->iterateIndex.index = groups[message->iterateIndex.group].itemCount - 1;
} else {
message->iterateIndex.index--;
}
return true;
}
int64_t MeasureItems(EsListViewIndex groupIndex, EsListViewIndex firstIndex, EsListViewIndex count) {
if (count == 0) return 0;
EsAssert(count > 0);
bool variableSize = flags & ES_LIST_VIEW_VARIABLE_SIZE;
if (!variableSize) {
ListViewGroup *group = &groups[groupIndex];
int64_t normalCount = count;
int64_t additionalSize = 0;
if ((group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) && firstIndex == 0) {
normalCount--;
additionalSize += fixedHeaderSize;
}
if ((group->flags & ES_LIST_VIEW_GROUP_HAS_FOOTER) && firstIndex + count == group->itemCount) {
normalCount--;
additionalSize += fixedFooterSize;
}
return additionalSize + normalCount * (flags & ES_LIST_VIEW_HORIZONTAL ? fixedWidth : fixedHeight);
}
if (count > 1) {
EsMessage m = { ES_MSG_LIST_VIEW_MEASURE_RANGE };
m.itemRange.group = groupIndex;
m.itemRange.firstIndex = firstIndex;
m.itemRange.count = count;
if (ES_HANDLED == EsMessageSend(this, &m)) {
return m.itemRange.result;
}
}
EsMessage m = {};
m.iterateIndex.group = groupIndex;
m.iterateIndex.index = firstIndex;
int64_t total = 0;
int64_t _count = 0;
while (true) {
EsMessage m2 = { ES_MSG_LIST_VIEW_MEASURE_ITEM };
m2.measureItem.group = groupIndex;
m2.measureItem.index = m.iterateIndex.index;
EsAssert(ES_HANDLED == EsMessageSend(this, &m2)); // Variable height list view must be able to measure items.
total += m2.measureItem.result;
_count++;
if (count == _count) {
return total;
}
IterateForwards(&m);
EsAssert(groupIndex == m.iterateIndex.group); // Index range did not exist in group.
}
}
void GetItemPosition(EsListViewIndex groupIndex, EsListViewIndex index, int64_t *_position, int64_t *_itemSize) {
int64_t gapBetweenGroup = style->gapMajor,
gapBetweenItems = (flags & ES_LIST_VIEW_TILED) ? style->gapWrap : style->gapMinor,
fixedSize = (flags & ES_LIST_VIEW_VARIABLE_SIZE) ? 0 : (flags & ES_LIST_VIEW_HORIZONTAL ? fixedWidth : fixedHeight),
startInset = flags & ES_LIST_VIEW_HORIZONTAL ? style->insets.l : style->insets.t;
int64_t position = (flags & ES_LIST_VIEW_HORIZONTAL ? -scroll.position[0] : -scroll.position[1]) + startInset,
itemSize = 0;
EsListViewIndex targetIndex = index;
for (EsListViewIndex i = 0; i < groupIndex; i++) {
position += groups[i].totalSize + gapBetweenGroup;
}
ListViewGroup *group = &groups[groupIndex];
if ((group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) && index == 0) {
itemSize = fixedHeaderSize;
} else if ((group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) && index == group->itemCount - 1) {
position += group->totalSize - fixedFooterSize;
itemSize = fixedFooterSize;
} else if (~flags & ES_LIST_VIEW_VARIABLE_SIZE) {
intptr_t linearIndex = index;
if (group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) {
linearIndex--;
position += fixedHeaderSize + gapBetweenItems;
}
linearIndex /= GetItemsPerBand();
position += (fixedSize + gapBetweenItems) * linearIndex;
itemSize = fixedSize;
} else {
EsAssert(~flags & ES_LIST_VIEW_TILED); // Tiled list views must be fixed-size.
EsMessage index = {};
index.type = ES_MSG_LIST_VIEW_FIND_POSITION;
index.iterateIndex.group = groupIndex;
index.iterateIndex.index = targetIndex;
if (ES_HANDLED == EsMessageSend(this, &index)) {
position += index.iterateIndex.position;
} else {
if (group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) {
position += fixedHeaderSize + gapBetweenItems;
}
bool forwards;
ListViewItem *reference = visibleItems.Length() ? visibleItems.array : nullptr;
bool closerToStartThanReference = reference && targetIndex < reference->index / 2;
bool closerToEndThanReference = reference && targetIndex > reference->index / 2 + (intptr_t) group->itemCount / 2;
if (reference && reference->group == groupIndex && !closerToStartThanReference && !closerToEndThanReference) {
index.iterateIndex.index = reference->index;
position = (flags & ES_LIST_VIEW_HORIZONTAL) ? reference->element->offsetX : reference->element->offsetY;
forwards = reference->index < targetIndex;
EsMessage firstIndex = {};
firstIndex.iterateIndex.group = groupIndex;
GetFirstIndex(&firstIndex);
if (index.iterateIndex.index == firstIndex.iterateIndex.index) {
forwards = true;
}
} else if (targetIndex > group->itemCount / 2) {
GetLastIndex(&index);
position += group->totalSize;
position -= MeasureItems(index.iterateIndex.group, index.iterateIndex.index, 1);
forwards = false;
} else {
GetFirstIndex(&index);
forwards = true;
}
while (index.iterateIndex.index != targetIndex) {
int64_t size = MeasureItems(index.iterateIndex.group, index.iterateIndex.index, 1);
position += forwards ? (size + gapBetweenItems) : -(size + gapBetweenItems);
EsAssert((forwards ? IterateForwards(&index) : IterateBackwards(&index)) && index.iterateIndex.group == groupIndex);
// Could not find the item in the group.
}
itemSize = MeasureItems(index.iterateIndex.group, index.iterateIndex.index, 1);
}
}
*_position = position;
*_itemSize = itemSize;
}
void EnsureItemVisible(EsListViewIndex groupIndex, EsListViewIndex index, uint8_t visibleFlags) {
ensureVisibleGroupIndex = groupIndex;
ensureVisibleIndex = index;
ensureVisibleFlags = visibleFlags;
if (!ensureVisibleQueued) {
UpdateAction action = {};
action.element = this;
action.callback = ListViewEnsureVisibleActionCallback;
window->updateActions.Add(action);
ensureVisibleQueued = true;
}
}
void _EnsureItemVisible(EsListViewIndex groupIndex, EsListViewIndex index, uint8_t visibleFlags) {
EsRectangle contentBounds = GetListBounds();
int64_t startInset = flags & ES_LIST_VIEW_HORIZONTAL ? style->insets.l : style->insets.t,
endInset = flags & ES_LIST_VIEW_HORIZONTAL ? style->insets.r : style->insets.b,
contentSize = flags & ES_LIST_VIEW_HORIZONTAL ? Width(contentBounds) : Height(contentBounds);
int64_t position, itemSize;
GetItemPosition(groupIndex, index, &position, &itemSize);
if (visibleFlags & ENSURE_VISIBLE_ALIGN_FOR_SCROLL_ITEM) {
if (flags & ES_LIST_VIEW_HORIZONTAL) {
scroll.SetX(scroll.position[0] + position - scrollItemOffset);
} else {
scroll.SetY(scroll.position[1] + position - scrollItemOffset);
}
useScrollItem = true;
return;
}
if (position >= 0 && position + itemSize <= contentSize - endInset) {
return;
}
if (visibleFlags & ENSURE_VISIBLE_ALIGN_TOP) {
if (flags & ES_LIST_VIEW_HORIZONTAL) {
scroll.SetX(scroll.position[0] + position - startInset);
} else {
scroll.SetY(scroll.position[1] + position - startInset);
}
} else if (visibleFlags & ENSURE_VISIBLE_ALIGN_CENTER) {
if (flags & ES_LIST_VIEW_HORIZONTAL) {
scroll.SetX(scroll.position[0] + position + itemSize / 2 - contentSize / 2);
} else {
scroll.SetY(scroll.position[1] + position + itemSize / 2 - contentSize / 2);
}
} else {
if (flags & ES_LIST_VIEW_HORIZONTAL) {
scroll.SetX(scroll.position[0] + position + itemSize - contentSize + endInset);
} else {
scroll.SetY(scroll.position[1] + position + itemSize - contentSize + endInset);
}
}
}
EsMessage FindFirstVisibleItem(int64_t *_position, int64_t position, ListViewItem *reference, bool *noItems) {
int64_t gapBetweenGroup = style->gapMajor,
gapBetweenItems = (flags & ES_LIST_VIEW_TILED) ? style->gapWrap : style->gapMinor,
fixedSize = (flags & ES_LIST_VIEW_VARIABLE_SIZE) ? 0 : (flags & ES_LIST_VIEW_HORIZONTAL ? fixedWidth : fixedHeight);
// Find the group.
// TODO Faster searching when there are many groups.
EsListViewIndex groupIndex = 0;
bool foundGroup = false;
for (; groupIndex < (EsListViewIndex) groups.Length(); groupIndex++) {
ListViewGroup *group = &groups[groupIndex];
int64_t totalSize = group->totalSize;
if (position + totalSize > 0) {
foundGroup = true;
break;
}
position += totalSize + gapBetweenGroup;
}
if (!foundGroup) {
if (noItems) {
*noItems = true;
return {};
} else {
EsAssert(false); // Could not find the first visible item with the given scroll.
}
}
EsMessage index = {};
index.iterateIndex.group = groupIndex;
// Can we go directly to the item?
if (~flags & ES_LIST_VIEW_VARIABLE_SIZE) {
index.iterateIndex.index = 0;
intptr_t addHeader = 0;
ListViewGroup *group = &groups[groupIndex];
if (group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) {
if (position + fixedHeaderSize > 0) {
*_position = position;
return index;
}
position += fixedHeaderSize + gapBetweenItems;
addHeader = 1;
}
EsListViewIndex band = -position / (fixedSize + gapBetweenItems);
if (band < 0) band = 0;
position += band * (fixedSize + gapBetweenItems);
EsListViewIndex itemsPerBand = (flags & ES_LIST_VIEW_TILED) ? GetItemsPerBand() : 1;
index.iterateIndex.index = band * itemsPerBand + addHeader;
if (index.iterateIndex.index >= group->itemCount) {
index.iterateIndex.index = group->itemCount - 1;
position += (index.iterateIndex.index / itemsPerBand - band) * (fixedSize + gapBetweenItems);
}
*_position = position;
return index;
}
EsAssert(~flags & ES_LIST_VIEW_TILED); // Trying to use TILED mode with VARIABLE_SIZE mode.
// Try asking the application to find the item.
index.type = ES_MSG_LIST_VIEW_FIND_INDEX;
index.iterateIndex.position = -position;
if (ES_HANDLED == EsMessageSend(this, &index)) {
*_position = -index.iterateIndex.position;
return index;
}
// Find the item within the group, manually.
bool forwards;
if (reference && reference->group == groupIndex) {
int64_t referencePosition = (flags & ES_LIST_VIEW_HORIZONTAL) ? reference->element->offsetX : reference->element->offsetY;
if (AbsoluteInteger64(referencePosition) < AbsoluteInteger64(position)
&& AbsoluteInteger64(referencePosition) < AbsoluteInteger64(position + groups[groupIndex].totalSize)) {
index.iterateIndex.index = reference->index;
position = referencePosition; // Use previous first visible item as reference.
forwards = position < 0;
EsMessage firstIndex = {};
firstIndex.iterateIndex.group = groupIndex;
GetFirstIndex(&firstIndex);
if (index.iterateIndex.index == firstIndex.iterateIndex.index) {
forwards = true;
}
goto gotReference;
}
}
if (position + groups[groupIndex].totalSize / 2 >= 0) {
GetFirstIndex(&index); // Use start of group as reference.
forwards = true;
} else {
GetLastIndex(&index); // Use end of group as reference
position += groups[groupIndex].totalSize;
position -= MeasureItems(index.iterateIndex.group, index.iterateIndex.index, 1);
forwards = false;
}
gotReference:;
if (forwards) {
// Iterate forwards from reference point.
while (true) {
int64_t size = fixedSize ?: MeasureItems(index.iterateIndex.group, index.iterateIndex.index, 1);
if (position + size > 0) {
*_position = position;
return index;
}
EsAssert(IterateForwards(&index) && index.iterateIndex.group == groupIndex);
// No items in the group are visible. Maybe invalid scroll position?
position += size + gapBetweenItems;
}
} else {
// Iterate backwards from reference point.
while (true) {
if (position <= 0 || !IterateBackwards(&index)) {
*_position = position;
return index;
}
int64_t size = fixedSize ?: MeasureItems(index.iterateIndex.group, index.iterateIndex.index, 1);
EsAssert(index.iterateIndex.group == groupIndex);
// No items in the group are visible. Maybe invalid scroll position?
position -= size + gapBetweenItems;
}
}
}
void _Populate() {
// TODO Keep one item before and after the viewport, so tab traversal on custom elements works.
// TODO Always keep an item if it has FOCUS_WITHIN.
// - But maybe we shouldn't allow focusable elements in a list view.
if (!totalItemCount) {
return;
}
EsRectangle contentBounds = GetListBounds();
int64_t contentSize = flags & ES_LIST_VIEW_HORIZONTAL ? Width(contentBounds) : Height(contentBounds);
int64_t scroll = EsCRTfloor(flags & ES_LIST_VIEW_HORIZONTAL ? (this->scroll.position[0] - style->insets.l)
: (this->scroll.position[1] - style->insets.t));
int64_t position = 0;
bool noItems = false;
EsMessage currentItem = FindFirstVisibleItem(&position, -scroll, visibleItems.Length() ? visibleItems.array : nullptr, &noItems);
uintptr_t visibleIndex = 0;
int64_t wrapLimit = GetWrapLimit();
int64_t fixedMinorSize = (flags & ES_LIST_VIEW_HORIZONTAL) ? fixedHeight : fixedWidth;
intptr_t itemsPerBand = GetItemsPerBand();
intptr_t itemInBand = 0;
int64_t computedMinorGap = style->gapMinor;
int64_t minorPosition = 0;
int64_t centerOffset = (flags & ES_LIST_VIEW_CENTER_TILES)
? (wrapLimit - itemsPerBand * (fixedMinorSize + style->gapMinor) + style->gapMinor) / 2 : 0;
while (visibleIndex < visibleItems.Length()) {
// Remove visible items no longer visible, before the viewport.
ListViewItem *visibleItem = &visibleItems[visibleIndex];
int64_t visibleItemPosition = flags & ES_LIST_VIEW_HORIZONTAL ? visibleItem->element->offsetX : visibleItem->element->offsetY;
if (visibleItemPosition >= position) break;
visibleItem->element->index = visibleIndex;
visibleItem->element->Destroy();
visibleItems.Delete(visibleIndex);
}
while (position < contentSize && !noItems) {
ListViewItem *visibleItem = visibleIndex == visibleItems.Length() ? nullptr : &visibleItems[visibleIndex];
if (visibleItem && visibleItem->index == currentItem.iterateIndex.index
&& visibleItem->group == currentItem.iterateIndex.group) {
// This is already a visible item.
if (~flags & ES_LIST_VIEW_TILED) {
int64_t expectedPosition = (flags & ES_LIST_VIEW_HORIZONTAL)
? visibleItem->element->offsetX - contentBounds.l
: visibleItem->element->offsetY - contentBounds.t;
if (position < expectedPosition - 1 || position > expectedPosition + 1) {
EsPrint("Item in unexpected position: got %d, should have been %d; index %d, scroll %d.\n",
expectedPosition, position, visibleItem->index, scroll);
EsAssert(false);
}
}
} else {
// Add a new visible item.
ListViewItem empty = {};
visibleItems.Insert(empty, visibleIndex);
visibleItem = &visibleItems[visibleIndex];
visibleItem->group = currentItem.iterateIndex.group;
visibleItem->index = currentItem.iterateIndex.index;
visibleItem->size = MeasureItems(visibleItem->group, visibleItem->index, 1);
ListViewGroup *group = &groups[visibleItem->group];
EsStyleID style = 0;
if ((group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) && visibleItem->index == 0) {
style = headerItemStyle;
visibleItem->isHeader = true;
} else if ((group->flags & ES_LIST_VIEW_GROUP_HAS_FOOTER) && visibleItem->index == (intptr_t) group->itemCount - 1) {
style = footerItemStyle;
visibleItem->isFooter = true;
} else {
if (group->flags & ES_LIST_VIEW_GROUP_INDENT) {
visibleItem->indent++;
}
style = itemStyle;
}
visibleItem->element = (ListViewItemElement *) EsHeapAllocate(sizeof(ListViewItemElement), true);
visibleItem->element->Initialise(this, ES_CELL_FILL, nullptr, style);
visibleItem->element->index = visibleIndex;
visibleItem->element->cName = "list view item";
visibleItem->element->messageClass = ListViewProcessItemMessage;
if (hasFocusedItem && visibleItem->group == focusedItemGroup && visibleItem->index == focusedItemIndex) {
visibleItem->element->customStyleState |= THEME_STATE_FOCUSED_ITEM;
}
if (state & UI_STATE_FOCUSED) {
visibleItem->element->customStyleState |= THEME_STATE_LIST_FOCUSED;
}
EsMessage m = {};
m.type = ES_MSG_LIST_VIEW_IS_SELECTED;
m.selectItem.group = visibleItem->group;
m.selectItem.index = visibleItem->index;
EsMessageSend(this, &m);
if (m.selectItem.isSelected) visibleItem->element->customStyleState |= THEME_STATE_SELECTED;
m.type = ES_MSG_LIST_VIEW_CREATE_ITEM;
m.createItem.group = visibleItem->group;
m.createItem.index = visibleItem->index;
m.createItem.item = visibleItem->element;
EsMessageSend(this, &m);
m.type = ES_MSG_LIST_VIEW_GET_INDENT;
m.getIndent.group = visibleItem->group;
m.getIndent.index = visibleItem->index;
m.getIndent.indent = 0;
EsMessageSend(this, &m);
visibleItem->indent += m.getIndent.indent;
SelectPreview(visibleItems.Length() - 1);
visibleItem->element->MaybeRefreshStyle();
}
visibleItem->element->index = visibleIndex;
// Update the item's position.
ListViewGroup *group = &groups[visibleItem->group];
if ((flags & ES_LIST_VIEW_TILED) && !visibleItem->isHeader && !visibleItem->isFooter) {
if (flags & ES_LIST_VIEW_HORIZONTAL) {
visibleItem->element->InternalMove(fixedWidth, fixedHeight,
position + contentBounds.l, minorPosition + style->insets.t + contentBounds.t + centerOffset);
} else {
visibleItem->element->InternalMove(fixedWidth, fixedHeight,
minorPosition + style->insets.l + contentBounds.l + centerOffset, position + contentBounds.t);
}
minorPosition += computedMinorGap + fixedMinorSize;
itemInBand++;
bool endOfGroup = ((group->flags & ES_LIST_VIEW_GROUP_HAS_FOOTER) && currentItem.iterateIndex.index == group->itemCount - 2)
|| (currentItem.iterateIndex.index == group->itemCount - 1);
if (itemInBand == itemsPerBand || endOfGroup) {
minorPosition = 0;
itemInBand = 0;
position += (flags & ES_LIST_VIEW_HORIZONTAL) ? visibleItem->element->width : visibleItem->element->height;
if (!endOfGroup || (group->flags & ES_LIST_VIEW_GROUP_HAS_FOOTER)) position += style->gapWrap;
}
} else {
if (flags & ES_LIST_VIEW_HORIZONTAL) {
visibleItem->element->InternalMove(
visibleItem->size,
Height(contentBounds) - style->insets.t - style->insets.b - visibleItem->indent * style->gapWrap,
position + contentBounds.l,
style->insets.t - this->scroll.position[1] + visibleItem->indent * style->gapWrap + contentBounds.t);
position += visibleItem->element->width;
} else if ((flags & ES_LIST_VIEW_COLUMNS) && ((~flags & ES_LIST_VIEW_CHOICE_SELECT) || (this->scroll.enabled[0]))) {
int indent = visibleItem->indent * style->gapWrap;
int firstColumn = activeColumns.Length() ? (registeredColumns[activeColumns[0]].width * theming.scale + secondaryCellStyle->gapMajor) : 0;
visibleItem->startAtSecondColumn = indent > firstColumn;
if (indent > firstColumn) indent = firstColumn;
visibleItem->element->InternalMove(totalColumnWidth - indent, visibleItem->size,
indent - this->scroll.position[0] + contentBounds.l + style->insets.l, position + contentBounds.t);
position += visibleItem->element->height;
} else {
int indent = visibleItem->indent * style->gapWrap + style->insets.l;
visibleItem->element->InternalMove(Width(contentBounds) - indent - style->insets.r, visibleItem->size,
indent + contentBounds.l - this->scroll.position[0], position + contentBounds.t);
position += visibleItem->element->height;
}
if ((flags & ES_LIST_VIEW_TILED) && (group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) && currentItem.iterateIndex.index == 0) {
position += style->gapWrap;
}
}
// Go to the next item.
visibleIndex++;
EsListViewIndex previousGroup = currentItem.iterateIndex.group;
if (!IterateForwards(&currentItem)) break;
position += previousGroup == currentItem.iterateIndex.group ? (flags & ES_LIST_VIEW_TILED ? 0 : style->gapMinor) : style->gapMajor;
}
while (visibleIndex < visibleItems.Length()) {
// Remove visible items no longer visible, after the viewport.
ListViewItem *visibleItem = &visibleItems[visibleIndex];
visibleItem->element->index = visibleIndex;
visibleItem->element->Destroy();
visibleItems.Delete(visibleIndex);
}
if (inlineTextbox) {
ListViewItem *item = FindVisibleItem(inlineTextboxGroup, inlineTextboxIndex);
if (item) MoveInlineTextbox(item);
}
if (visibleItems.Length() && (!useScrollItem || !hasScrollItem)) {
scrollItemGroup = visibleItems.First().group;
scrollItemIndex = visibleItems.First().index;
scrollItemOffset = visibleItems.First().element->offsetY;
hasScrollItem = true;
}
}
void Populate() {
if (!populateQueued) {
UpdateAction action = {};
action.element = this;
action.callback = ListViewPopulateActionCallback;
window->updateActions.Add(action);
populateQueued = true;
}
}
void Wrap(bool sizeChanged) {
if (~flags & ES_LIST_VIEW_TILED) return;
totalSize = 0;
intptr_t itemsPerBand = GetItemsPerBand();
for (uintptr_t i = 0; i < groups.Length(); i++) {
ListViewGroup *group = &groups[i];
int64_t groupSize = 0;
intptr_t itemCount = group->itemCount;
if (group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) {
groupSize += fixedHeaderSize + style->gapWrap;
itemCount--;
}
if (group->flags & ES_LIST_VIEW_GROUP_HAS_FOOTER) {
groupSize += fixedFooterSize + style->gapWrap;
itemCount--;
}
intptr_t bandsInGroup = (itemCount + itemsPerBand - 1) / itemsPerBand;
groupSize += (((flags & ES_LIST_VIEW_HORIZONTAL) ? fixedWidth : fixedHeight) + style->gapWrap) * bandsInGroup;
groupSize -= style->gapWrap;
group->totalSize = groupSize;
totalSize += groupSize + (group == &groups.Last() ? 0 : style->gapMajor);
}
scroll.Refresh();
if (sizeChanged) {
useScrollItem = true;
}
if (useScrollItem && hasScrollItem) {
EnsureItemVisible(scrollItemGroup, scrollItemIndex, ENSURE_VISIBLE_ALIGN_FOR_SCROLL_ITEM);
}
}
void InsertSpace(int64_t space, uintptr_t beforeItem) {
if (!space) return;
if (flags & ES_LIST_VIEW_TILED) {
EsElementUpdateContentSize(this);
return;
}
totalSize += space;
for (uintptr_t i = beforeItem; i < visibleItems.Length(); i++) {
ListViewItem *item = &visibleItems[i];
if (flags & ES_LIST_VIEW_HORIZONTAL) {
item->element->offsetX += space;
} else {
item->element->offsetY += space;
}
}
useScrollItem = false;
scroll.Refresh();
EsElementUpdateContentSize(this);
}
void SetSelected(EsListViewIndex fromGroup, EsListViewIndex fromIndex, EsListViewIndex toGroup, EsListViewIndex toIndex,
bool select, bool toggle,
intptr_t period = 0, intptr_t periodBegin = 0, intptr_t periodEnd = 0) {
if (!select && (flags & ES_LIST_VIEW_CHOICE_SELECT)) {
return;
}
if (!select && (flags & ES_LIST_VIEW_SINGLE_SELECT)) {
EsMessage m = {};
m.type = ES_MSG_LIST_VIEW_SELECT;
m.selectItem.isSelected = false;
fixedItemSelection = -1;
EsMessageSend(this, &m);
return;
}
if (fromGroup == toGroup && fromIndex > toIndex) {
EsListViewIndex temp = fromIndex;
fromIndex = toIndex;
toIndex = temp;
} else if (fromGroup > toGroup) {
EsListViewIndex temp1 = fromGroup;
fromGroup = toGroup;
toGroup = temp1;
EsListViewIndex temp2 = fromIndex;
fromIndex = toIndex;
toIndex = temp2;
}
EsMessage start = {}, end = {};
start.iterateIndex.group = fromGroup;
end.iterateIndex.group = fromGroup;
for (; start.iterateIndex.group <= toGroup; start.iterateIndex.group++, end.iterateIndex.group++) {
if (start.iterateIndex.group == fromGroup) {
start.iterateIndex.index = fromIndex;
} else {
GetFirstIndex(&start);
}
if (end.iterateIndex.group == toGroup) {
end.iterateIndex.index = toIndex;
} else {
GetLastIndex(&end);
}
EsMessage m = { ES_MSG_LIST_VIEW_SELECT_RANGE };
m.selectRange.group = start.iterateIndex.group;
m.selectRange.fromIndex = start.iterateIndex.index;
m.selectRange.toIndex = end.iterateIndex.index;
m.selectRange.select = select;
m.selectRange.toggle = toggle;
if (!period && 0 != EsMessageSend(this, &m)) {
continue;
}
intptr_t linearIndex = 0;
while (true) {
m.selectItem.group = start.iterateIndex.group;
m.selectItem.index = start.iterateIndex.index;
m.selectItem.isSelected = select;
if (period) {
ListViewGroup *group = &groups[m.selectItem.group];
intptr_t i = linearIndex;
if (group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) {
if (linearIndex == 0) {
goto process;
} else {
i--;
}
}
if (group->flags & ES_LIST_VIEW_GROUP_HAS_FOOTER) {
if (linearIndex == (intptr_t) group->itemCount - 1) {
goto process;
}
}
i %= period;
if (i < periodBegin || i > periodEnd) {
goto ignore;
}
}
process:;
if (toggle) {
m.type = ES_MSG_LIST_VIEW_IS_SELECTED;
EsMessageSend(this, &m);
m.selectItem.isSelected = !m.selectItem.isSelected;
}
m.type = ES_MSG_LIST_VIEW_SELECT;
if (flags & ES_LIST_VIEW_FIXED_ITEMS) {
fixedItemSelection = m.selectItem.index;
EsAssert((uintptr_t) m.selectItem.index < fixedItems.Length());
m.selectItem.index = fixedItemIndices[m.selectItem.index];
}
EsMessageSend(this, &m);
ignore:;
if (start.iterateIndex.index == end.iterateIndex.index) {
break;
}
IterateForwards(&start);
linearIndex++;
EsAssert(start.iterateIndex.group == end.iterateIndex.group); // The from and to selection indices in the group were incorrectly ordered.
}
}
}
void SelectPreview(intptr_t singleItem = -1) {
if (!hasSelectionBoxAnchor) {
return;
}
int64_t x1 = selectionBoxPositionX, x2 = selectionBoxAnchorX,
y1 = selectionBoxPositionY, y2 = selectionBoxAnchorY;
if (x1 > x2) { int64_t temp = x1; x1 = x2; x2 = temp; }
if (y1 > y2) { int64_t temp = y1; y1 = y2; y2 = temp; }
x1 -= scroll.position[0], x2 -= scroll.position[0];
y1 -= scroll.position[1], y2 -= scroll.position[1];
EsRectangle bounds = GetListBounds();
if (x1 < -1000) x1 = -1000;
if (x2 < -1000) x2 = -1000;
if (y1 < -1000) y1 = -1000;
if (y2 < -1000) y2 = -1000;
if (x1 > bounds.r + 1000) x1 = bounds.r + 1000;
if (x2 > bounds.r + 1000) x2 = bounds.r + 1000;
if (y1 > bounds.b + 1000) y1 = bounds.b + 1000;
if (y2 > bounds.b + 1000) y2 = bounds.b + 1000;
selectionBox->InternalMove(x2 - x1, y2 - y1, x1, y1);
for (uintptr_t i = 0; i < visibleItems.Length(); i++) {
if (singleItem != -1) {
i = singleItem;
}
EsMessage m = { ES_MSG_LIST_VIEW_IS_SELECTED };
m.selectItem.index = visibleItems[i].index;
m.selectItem.group = visibleItems[i].group;
EsMessageSend(this, &m);
EsElement *item = visibleItems[i].element;
if (x1 < item->offsetX + item->width && x2 >= item->offsetX && y1 < item->offsetY + item->height && y2 >= item->offsetY) {
if (EsKeyboardIsCtrlHeld()) {
m.selectItem.isSelected = !m.selectItem.isSelected;
} else {
m.selectItem.isSelected = true;
}
}
if (m.selectItem.isSelected) {
item->customStyleState |= THEME_STATE_SELECTED;
} else {
item->customStyleState &= ~THEME_STATE_SELECTED;
}
item->MaybeRefreshStyle();
if (singleItem != -1) {
break;
}
}
}
intptr_t GetItemsPerBand() {
if (~flags & ES_LIST_VIEW_TILED) {
return 1;
} else {
int64_t wrapLimit = GetWrapLimit();
int64_t fixedMinorSize = (flags & ES_LIST_VIEW_HORIZONTAL) ? fixedHeight : fixedWidth;
intptr_t itemsPerBand = fixedMinorSize && ((fixedMinorSize + style->gapMinor) < wrapLimit)
? (wrapLimit / (fixedMinorSize + style->gapMinor)) : 1;
return MinimumInteger(itemsPerBand, maximumItemsPerBand);
}
}
void SelectBox(int64_t x1, int64_t x2, int64_t y1, int64_t y2, bool toggle) {
if (!totalItemCount) {
return;
}
EsRectangle contentBounds = GetListBounds();
int64_t offset = 0;
EsMessage start, end;
bool noItems = false;
if (flags & ES_LIST_VIEW_HORIZONTAL) {
if (y1 >= contentBounds.b - style->insets.b || y2 < contentBounds.t + style->insets.t) {
return;
}
} else if (flags & ES_LIST_VIEW_COLUMNS) {
if (x1 >= contentBounds.l + style->insets.l + totalColumnWidth || x2 < contentBounds.l + style->insets.l) {
return;
}
} else {
if (x1 >= contentBounds.r - style->insets.r || x2 < contentBounds.l + style->insets.l) {
return;
}
}
// TODO Use reference for FindFirstVisibleItem.
bool adjustStart = false, adjustEnd = false;
int r1 = (flags & ES_LIST_VIEW_HORIZONTAL) ? style->insets.l - x1 : style->insets.t - y1 + scroll.fixedViewport[1];
int r2 = (flags & ES_LIST_VIEW_HORIZONTAL) ? style->insets.l - x2 : style->insets.t - y2 + scroll.fixedViewport[1];
start = FindFirstVisibleItem(&offset, r1, nullptr, &noItems);
if (noItems) return;
adjustStart = -offset >= MeasureItems(start.iterateIndex.group, start.iterateIndex.index, 1);
end = FindFirstVisibleItem(&offset, r2, nullptr, &noItems);
adjustEnd = !noItems;
if (noItems) { end.iterateIndex.group = groups.Length() - 1; GetLastIndex(&end); }
if (flags & ES_LIST_VIEW_TILED) {
int64_t wrapLimit = GetWrapLimit();
int64_t fixedMinorSize = (flags & ES_LIST_VIEW_HORIZONTAL) ? fixedHeight : fixedWidth;
intptr_t itemsPerBand = GetItemsPerBand();
int64_t centerOffset = (flags & ES_LIST_VIEW_CENTER_TILES) ? (wrapLimit - itemsPerBand * (fixedMinorSize + style->gapMinor) + style->gapMinor) / 2 : 0;
int64_t minorStartOffset = centerOffset + ((flags & ES_LIST_VIEW_HORIZONTAL) ? style->insets.t : style->insets.l);
int64_t s0 = (flags & ES_LIST_VIEW_HORIZONTAL) ? y1 : x1;
int64_t s1 = (flags & ES_LIST_VIEW_HORIZONTAL) ? y2 : x2;
int64_t startEdge = minorStartOffset;
int64_t endEdge = minorStartOffset + (fixedMinorSize + style->gapMinor) * itemsPerBand - style->gapMinor;
if (s1 < startEdge || s0 >= endEdge) return;
if (s0 < startEdge) s0 = startEdge;
if (s1 >= endEdge) s1 = endEdge - 1;
intptr_t startInBand = (s0 - startEdge + style->gapMinor /* round up if in gap */) / (fixedMinorSize + style->gapMinor);
intptr_t endInBand = (s1 - startEdge) / (fixedMinorSize + style->gapMinor);
if (startInBand > endInBand) return;
if (adjustStart) {
ListViewGroup *group = &groups[start.iterateIndex.group];
if (((group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) && start.iterateIndex.index == 0)
|| ((group->flags & ES_LIST_VIEW_GROUP_HAS_FOOTER) && start.iterateIndex.index == group->itemCount - 1)) {
IterateForwards(&start);
} else {
for (intptr_t i = 0; i < itemsPerBand; i++) {
if ((group->flags & ES_LIST_VIEW_GROUP_HAS_FOOTER) && start.iterateIndex.index == group->itemCount - 1) {
break;
}
IterateForwards(&start);
}
}
}
if (adjustEnd) {
ListViewGroup *group = &groups[end.iterateIndex.group];
if (((group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) && end.iterateIndex.index == 0)
|| ((group->flags & ES_LIST_VIEW_GROUP_HAS_FOOTER) && end.iterateIndex.index == group->itemCount - 1)) {
} else {
for (intptr_t i = 0; i < itemsPerBand - 1; i++) {
if ((group->flags & ES_LIST_VIEW_GROUP_HAS_FOOTER) && end.iterateIndex.index == group->itemCount - 1) {
IterateBackwards(&end);
break;
}
IterateForwards(&end);
}
}
}
if ((start.iterateIndex.group == end.iterateIndex.group && start.iterateIndex.index > end.iterateIndex.index)
|| (start.iterateIndex.group > end.iterateIndex.group)) {
return;
}
SetSelected(start.iterateIndex.group, start.iterateIndex.index, end.iterateIndex.group, end.iterateIndex.index, true, toggle,
itemsPerBand, startInBand, endInBand);
} else {
if (adjustStart) {
IterateForwards(&start);
}
SetSelected(start.iterateIndex.group, start.iterateIndex.index, end.iterateIndex.group, end.iterateIndex.index, true, toggle);
}
}
void UpdateVisibleItemSelectionState(uintptr_t i) {
EsMessage m = {};
m.type = ES_MSG_LIST_VIEW_IS_SELECTED;
m.selectItem.group = visibleItems[i].group;
m.selectItem.index = visibleItems[i].index;
EsMessageSend(this, &m);
if (m.selectItem.isSelected) {
visibleItems[i].element->customStyleState |= THEME_STATE_SELECTED;
} else {
visibleItems[i].element->customStyleState &= ~THEME_STATE_SELECTED;
}
visibleItems[i].element->MaybeRefreshStyle();
}
void UpdateVisibleItemsSelectionState() {
for (uintptr_t i = 0; i < visibleItems.Length(); i++) {
UpdateVisibleItemSelectionState(i);
}
}
void Select(EsListViewIndex group, EsListViewIndex index, bool range, bool toggle, bool moveAnchorOnly) {
if ((~flags & ES_LIST_VIEW_SINGLE_SELECT) && (~flags & ES_LIST_VIEW_MULTI_SELECT) && (~flags & ES_LIST_VIEW_CHOICE_SELECT)) {
return;
}
if (!totalItemCount) {
return;
}
if (!hasAnchorItem || (~flags & ES_LIST_VIEW_MULTI_SELECT)) {
range = false;
}
bool emptySpace = false;
if (group == -1) {
// Clicked on empty space.
if (range || toggle) return;
emptySpace = true;
}
if (!range && !emptySpace) {
hasAnchorItem = true;
anchorItemGroup = group;
anchorItemIndex = index;
}
if (moveAnchorOnly) {
return;
}
if (!toggle) {
// Clear existing selection.
SetSelected(0, 0, groups.Length() - 1, groups.Last().itemCount - 1, false, false);
}
if (range) {
// Select range.
SetSelected(anchorItemGroup, anchorItemIndex, group, index, true, false);
} else if (toggle) {
// Toggle single item.
SetSelected(group, index, group, index, false, true);
} else if (!emptySpace) {
// Select single item.
SetSelected(group, index, group, index, true, false);
}
UpdateVisibleItemsSelectionState();
}
int ProcessItemMessage(uintptr_t visibleIndex, EsMessage *message, ListViewItemElement *element) {
ListViewItem *item = &visibleItems[visibleIndex];
if (message->type == ES_MSG_PAINT) {
EsMessage m = { ES_MSG_LIST_VIEW_GET_CONTENT };
uint8_t _buffer[512];
EsBuffer buffer = { .out = _buffer, .bytes = sizeof(_buffer) };
m.getContent.buffer = &buffer;
m.getContent.index = item->index;
m.getContent.group = item->group;
EsTextSelection selection = {};
selection.hideCaret = true;
if (searchBufferBytes && item->showSearchHighlight) {
// TODO We might need to store the matched bytes per item, because of case insensitivity.
selection.caret1 = searchBufferBytes;
selection.hideCaret = false;
}
if (flags & ES_LIST_VIEW_COLUMNS) {
EsRectangle bounds = EsRectangleAddBorder(element->GetBounds(), element->style->insets);
for (uintptr_t i = item->startAtSecondColumn ? 1 : 0; i < activeColumns.Length(); i++) {
m.getContent.activeColumnIndex = i;
m.getContent.columnID = registeredColumns[activeColumns[i]].id;
m.getContent.icon = 0;
buffer.position = 0;
bounds.r = bounds.l + registeredColumns[activeColumns[i]].width * theming.scale
- element->style->insets.r - element->style->insets.l;
if (i == 0) {
bounds.r -= item->indent * style->gapWrap;
}
EsRectangle drawBounds = { bounds.l + message->painter->offsetX, bounds.r + message->painter->offsetX,
bounds.t + message->painter->offsetY, bounds.b + message->painter->offsetY };
if (EsRectangleClip(drawBounds, message->painter->clip, nullptr)
&& ES_HANDLED == EsMessageSend(this, &m)) {
bool useSelectedCellStyle = (item->element->customStyleState & THEME_STATE_SELECTED) && (flags & ES_LIST_VIEW_CHOICE_SELECT);
UIStyle *style = useSelectedCellStyle ? selectedCellStyle : i ? secondaryCellStyle : primaryCellStyle;
style->PaintText(message->painter, element, bounds,
(char *) _buffer, buffer.position, m.getContent.icon,
registeredColumns[activeColumns[i]].flags, i ? nullptr : &selection);
}
bounds.l += registeredColumns[activeColumns[i]].width * theming.scale + secondaryCellStyle->gapMajor;
if (i == 0) {
bounds.l -= item->indent * style->gapWrap;
}
}
} else {
if (flags & ES_LIST_VIEW_TILED) {
m.type = ES_MSG_LIST_VIEW_GET_SUMMARY;
if (ES_HANDLED == EsMessageSend(this, &m)) goto standardPaint;
m.type = ES_MSG_LIST_VIEW_GET_CONTENT;
}
if (ES_HANDLED == EsMessageSend(this, &m)) goto standardPaint;
return 0;
standardPaint:;
if (inlineTextbox && inlineTextboxGroup == item->group && inlineTextboxIndex == item->index) {
buffer.position = 0;
}
EsDrawContent(message->painter, element, element->GetBounds(),
(char *) _buffer, buffer.position, m.getContent.icon,
m.getContent.drawContentFlags, &selection);
}
} else if (message->type == ES_MSG_LAYOUT) {
if (element->GetChildCount()) {
EsElement *child = element->GetChild(0);
EsRectangle bounds = element->GetBounds();
child->InternalMove(bounds.r - bounds.l, bounds.b - bounds.t, bounds.l, bounds.t);
}
} else if (message->type == ES_MSG_MOUSE_LEFT_DOWN || message->type == ES_MSG_MOUSE_MIDDLE_CLICK) {
EsElementFocus(this);
if (hasFocusedItem) {
ListViewItem *oldFocus = FindVisibleItem(focusedItemGroup, focusedItemIndex);
if (oldFocus) {
oldFocus->element->customStyleState &= ~THEME_STATE_FOCUSED_ITEM;
oldFocus->element->MaybeRefreshStyle();
}
}
hasFocusedItem = true;
focusedItemGroup = item->group;
focusedItemIndex = item->index;
element->customStyleState |= THEME_STATE_FOCUSED_ITEM;
if (message->type == ES_MSG_MOUSE_MIDDLE_CLICK) {
Select(item->group, item->index, false, false, false);
EsMessage m = { ES_MSG_LIST_VIEW_CHOOSE_ITEM };
m.chooseItem.group = item->group;
m.chooseItem.index = item->index;
m.chooseItem.source = ES_LIST_VIEW_CHOOSE_ITEM_MIDDLE_CLICK;
EsMessageSend(this, &m);
} else if (message->mouseDown.clickChainCount == 1 || (~element->customStyleState & THEME_STATE_SELECTED)) {
Select(item->group, item->index, EsKeyboardIsShiftHeld(), EsKeyboardIsCtrlHeld(), false);
} else if (message->mouseDown.clickChainCount == 2) {
EsMessage m = { ES_MSG_LIST_VIEW_CHOOSE_ITEM };
m.chooseItem.group = item->group;
m.chooseItem.index = item->index;
m.chooseItem.source = ES_LIST_VIEW_CHOOSE_ITEM_DOUBLE_CLICK;
EsMessageSend(this, &m);
}
} else if (message->type == ES_MSG_MOUSE_MIDDLE_DOWN) {
} else if (message->type == ES_MSG_MOUSE_RIGHT_DOWN) {
EsMessage m = { ES_MSG_LIST_VIEW_IS_SELECTED };
m.selectItem.index = item->index;
m.selectItem.group = item->group;
EsMessageSend(this, &m);
if (!m.selectItem.isSelected) {
Select(item->group, item->index, EsKeyboardIsShiftHeld(), EsKeyboardIsCtrlHeld(), false);
}
m.type = ES_MSG_LIST_VIEW_CONTEXT_MENU;
EsMessageSend(this, &m);
} else if (message->type == ES_MSG_MOUSE_LEFT_DRAG) {
if (flags & ES_LIST_VIEW_CHOICE_SELECT) {
window->pressed = this;
ProcessMessage(message);
}
} else if (message->type == ES_MSG_GET_INSPECTOR_INFORMATION) {
EsMessage m = { ES_MSG_LIST_VIEW_GET_CONTENT };
uint8_t _buffer[256];
EsBuffer buffer = { .out = _buffer, .bytes = sizeof(_buffer) };
m.getContent.buffer = &buffer;
m.getContent.index = item->index;
m.getContent.group = item->group;
EsMessageSend(this, &m);
EsBufferFormat(message->getContent.buffer, "index %d '%s'", item->index, buffer.position, buffer.out);
} else {
return 0;
}
return ES_HANDLED;
}
inline int GetWrapLimit() {
EsRectangle bounds = GetListBounds();
return (flags & ES_LIST_VIEW_HORIZONTAL)
? bounds.b - bounds.t - style->insets.b - style->insets.t
: bounds.r - bounds.l - style->insets.r - style->insets.l;
}
ListViewItem *FindVisibleItem(EsListViewIndex group, EsListViewIndex index) {
for (uintptr_t i = 0; i < visibleItems.Length(); i++) {
ListViewItem *item = &visibleItems[i];
if (item->group == group && item->index == index) {
return item;
}
}
return nullptr;
}
bool Search() {
uint8_t _buffer[64];
if (!hasFocusedItem) {
// Select the first item in the list.
KeyInput(ES_SCANCODE_DOWN_ARROW, false, false, false, true);
if (!hasFocusedItem) return false;
}
EsMessage m = { ES_MSG_LIST_VIEW_SEARCH };
m.searchItem.index = focusedItemIndex;
m.searchItem.group = focusedItemGroup;
m.searchItem.query = searchBuffer;
m.searchItem.queryBytes = searchBufferBytes;
int response = EsMessageSend(this, &m);
if (response == ES_REJECTED) {
return false;
}
ListViewItem *oldFocus = FindVisibleItem(focusedItemGroup, focusedItemIndex);
if (oldFocus) {
oldFocus->element->customStyleState &= ~THEME_STATE_FOCUSED_ITEM;
oldFocus->element->MaybeRefreshStyle();
}
bool found = false;
if (response == ES_HANDLED) {
focusedItemIndex = m.searchItem.index;
focusedItemGroup = m.searchItem.group;
found = true;
} else {
EsMessage m = { ES_MSG_LIST_VIEW_GET_CONTENT };
EsBuffer buffer = { .out = _buffer, .bytes = sizeof(_buffer) };
m.getContent.buffer = &buffer;
m.getContent.index = focusedItemIndex;
m.getContent.group = focusedItemGroup;
EsMessage m2 = {};
m2.iterateIndex.index = focusedItemIndex;
m2.iterateIndex.group = focusedItemGroup;
do {
buffer.position = 0;
EsMessageSend(this, &m);
if (StringStartsWith((char *) _buffer, buffer.position, searchBuffer, searchBufferBytes, true)) {
found = true;
break;
}
if (!IterateForwards(&m2)) {
m2.iterateIndex.group = 0;
GetFirstIndex(&m2);
}
m.getContent.index = m2.iterateIndex.index;
m.getContent.group = m2.iterateIndex.group;
} while (m.getContent.index != focusedItemIndex || m.getContent.group != focusedItemGroup);
focusedItemIndex = m.getContent.index;
focusedItemGroup = m.getContent.group;
EnsureItemVisible(focusedItemGroup, focusedItemIndex, ES_FLAGS_DEFAULT);
}
ListViewItem *newFocus = FindVisibleItem(focusedItemGroup, focusedItemIndex);
if (newFocus) {
newFocus->element->customStyleState |= THEME_STATE_FOCUSED_ITEM;
newFocus->element->MaybeRefreshStyle();
}
{
EsMessage m = { ES_MSG_LIST_VIEW_GET_CONTENT };
EsBuffer buffer = { .out = _buffer, .bytes = sizeof(_buffer) };
m.getContent.buffer = &buffer;
for (uintptr_t i = 0; i < visibleItems.Length(); i++) {
ListViewItem *item = &visibleItems[i];
m.getContent.index = item->index;
m.getContent.group = item->group;
buffer.position = 0;
EsMessageSend(this, &m);
bool shouldShowSearchHighlight = StringStartsWith((char *) _buffer, buffer.position, searchBuffer, searchBufferBytes, true);
if (shouldShowSearchHighlight || (!shouldShowSearchHighlight && item->showSearchHighlight)) {
item->showSearchHighlight = shouldShowSearchHighlight;
item->element->Repaint(true);
}
}
}
Select(-1, 0, false, false, false);
Select(focusedItemGroup, focusedItemIndex, false, false, false);
return found;
}
bool KeyInput(int scancode, bool ctrl, bool alt, bool shift, bool keepSearchBuffer = false) {
if (!totalItemCount || alt) {
return false;
}
if (scancode == ES_SCANCODE_BACKSPACE && searchBufferBytes) {
searchBufferBytes = 0;
Search();
return true;
}
bool isNext = false,
isPrevious = false,
isNextBand = false,
isPreviousBand = false,
isHome = scancode == ES_SCANCODE_HOME,
isEnd = scancode == ES_SCANCODE_END,
isPageUp = scancode == ES_SCANCODE_PAGE_UP,
isPageDown = scancode == ES_SCANCODE_PAGE_DOWN,
isSpace = scancode == ES_SCANCODE_SPACE,
isEnter = scancode == ES_SCANCODE_ENTER;
if (flags & ES_LIST_VIEW_HORIZONTAL) {
isNext = scancode == ES_SCANCODE_DOWN_ARROW;
isNextBand = scancode == ES_SCANCODE_RIGHT_ARROW;
isPrevious = scancode == ES_SCANCODE_UP_ARROW;
isPreviousBand = scancode == ES_SCANCODE_LEFT_ARROW;
} else {
isNext = scancode == ES_SCANCODE_RIGHT_ARROW;
isNextBand = scancode == ES_SCANCODE_DOWN_ARROW;
isPrevious = scancode == ES_SCANCODE_LEFT_ARROW;
isPreviousBand = scancode == ES_SCANCODE_UP_ARROW;
}
if (hasSelectionBoxAnchor) {
if (scancode == ES_SCANCODE_UP_ARROW) scroll.SetY(scroll.position[1] - GetConstantNumber("scrollKeyMovement"));
if (scancode == ES_SCANCODE_DOWN_ARROW) scroll.SetY(scroll.position[1] + GetConstantNumber("scrollKeyMovement"));
if (scancode == ES_SCANCODE_LEFT_ARROW) scroll.SetX(scroll.position[0] - GetConstantNumber("scrollKeyMovement"));
if (scancode == ES_SCANCODE_RIGHT_ARROW) scroll.SetX(scroll.position[0] + GetConstantNumber("scrollKeyMovement"));
if (scancode == ES_SCANCODE_PAGE_UP) scroll.SetY(scroll.position[1] - Height(GetBounds()));
if (scancode == ES_SCANCODE_PAGE_DOWN) scroll.SetY(scroll.position[1] + Height(GetBounds()));
if (flags & ES_LIST_VIEW_HORIZONTAL) {
if (scancode == ES_SCANCODE_HOME) scroll.SetX(0);
if (scancode == ES_SCANCODE_END) scroll.SetX(scroll.limit[0]);
} else {
if (scancode == ES_SCANCODE_HOME) scroll.SetY(0);
if (scancode == ES_SCANCODE_END) scroll.SetY(scroll.limit[1]);
}
} else if (isPrevious || isNext || isHome || isEnd || isPageUp || isPageDown || isNextBand || isPreviousBand) {
ListViewItem *oldFocus = FindVisibleItem(focusedItemGroup, focusedItemIndex);
if (oldFocus) {
oldFocus->element->customStyleState &= ~THEME_STATE_FOCUSED_ITEM;
oldFocus->element->MaybeRefreshStyle();
}
EsMessage m = {};
if (hasFocusedItem && (isPrevious || isNext || isPageUp || isPageDown || isNextBand || isPreviousBand)) {
m.iterateIndex.group = focusedItemGroup;
m.iterateIndex.index = focusedItemIndex;
uintptr_t itemsPerBand = GetItemsPerBand();
for (uintptr_t i = 0; i < ((isPageUp || isPageDown) ? (10 * itemsPerBand) : (isNextBand || isPreviousBand) ? itemsPerBand : 1); i++) {
if (isNext || isPageDown || isNextBand) IterateForwards(&m);
else IterateBackwards(&m);
}
} else {
if (isNext || isNextBand || isHome) {
m.iterateIndex.group = 0;
GetFirstIndex(&m);
} else {
m.iterateIndex.group = groups.Length() - 1;
GetLastIndex(&m);
}
}
hasFocusedItem = true;
focusedItemGroup = m.iterateIndex.group;
focusedItemIndex = m.iterateIndex.index;
ListViewItem *newFocus = FindVisibleItem(focusedItemGroup, focusedItemIndex);
if (newFocus) {
newFocus->element->customStyleState |= THEME_STATE_FOCUSED_ITEM;
newFocus->element->MaybeRefreshStyle();
}
if (!keepSearchBuffer) ClearSearchBuffer();
EnsureItemVisible(focusedItemGroup, focusedItemIndex, (isPrevious || isHome || isPageUp || isPreviousBand) ? ENSURE_VISIBLE_ALIGN_TOP : ES_FLAGS_DEFAULT);
Select(focusedItemGroup, focusedItemIndex, shift, ctrl, ctrl && !shift);
return true;
} else if (isSpace && ctrl && !shift && hasFocusedItem) {
Select(focusedItemGroup, focusedItemIndex, false, true, false);
return true;
} else if (isEnter && hasFocusedItem) {
if (searchBufferBytes) {
searchBufferLastKeyTime = 0;
searchBufferBytes = 0;
EsListViewInvalidateAll(this);
}
EsMessage m = { ES_MSG_LIST_VIEW_CHOOSE_ITEM };
m.chooseItem.group = focusedItemGroup;
m.chooseItem.index = focusedItemIndex;
m.chooseItem.source = ES_LIST_VIEW_CHOOSE_ITEM_ENTER;
EsMessageSend(this, &m);
return true;
} else if (!ctrl && !alt) {
uint64_t currentTime = EsTimeStampMs();
if (searchBufferLastKeyTime + GetConstantNumber("listViewSearchBufferTimeout") < currentTime) {
searchBufferBytes = 0;
}
StartAnimating();
const char *inputString = KeyboardLayoutLookup(scancode, shift, false, false, false);
size_t inputStringBytes = EsCStringLength(inputString);
if (inputString && searchBufferBytes + inputStringBytes < sizeof(searchBuffer)) {
searchBufferLastKeyTime = currentTime;
EsMemoryCopy(searchBuffer + searchBufferBytes, inputString, inputStringBytes);
size_t previousSearchBufferBytes = searchBufferBytes;
searchBufferBytes += inputStringBytes;
if (!Search()) searchBufferBytes = previousSearchBufferBytes;
return true;
}
}
return false;
}
void ClearSearchBuffer() {
searchBufferBytes = 0;
for (uintptr_t i = 0; i < visibleItems.Length(); i++) {
if (visibleItems[i].showSearchHighlight) {
visibleItems[i].showSearchHighlight = false;
visibleItems[i].element->Repaint(true);
}
}
}
void DragSelect() {
EsRectangle bounds = GetWindowBounds();
EsPoint mouse = EsMouseGetPosition(window);
if (mouse.x < bounds.l) mouse.x = bounds.l;
if (mouse.x >= bounds.r) mouse.x = bounds.r - 1;
if (visibleItems.Length()) {
int32_t start = visibleItems[0].element->GetWindowBounds().t;
int32_t end = visibleItems.Last().element->GetWindowBounds().b;
if (mouse.y < start) mouse.y = start;
if (mouse.y >= end) mouse.y = end - 1;
}
EsElement *hoverItem = UIFindHoverElementRecursively(this, bounds.l - offsetX, bounds.t - offsetY, mouse);
if (hoverItem && hoverItem->messageClass == ListViewProcessItemMessage) {
EsMessage m = {};
m.type = ES_MSG_MOUSE_LEFT_DOWN;
EsMessageSend(hoverItem, &m);
}
}
void MoveInlineTextbox(ListViewItem *item) {
UIStyle *style = item->element->style;
if (flags & ES_LIST_VIEW_COLUMNS) {
int offset = primaryCellStyle->metrics->iconSize + primaryCellStyle->gapMinor
+ style->insets.l - inlineTextbox->style->insets.l;
inlineTextbox->InternalMove(registeredColumns[activeColumns[0]].width * theming.scale - offset, item->element->height,
item->element->offsetX + offset, item->element->offsetY);
} else if (flags & ES_LIST_VIEW_TILED) {
if (style->metrics->layoutVertical) {
int height = inlineTextbox->style->preferredHeight;
int textStart = style->metrics->iconSize + style->gapMinor + style->insets.t;
int textEnd = item->element->height - style->insets.b;
int offset = (textStart + textEnd - height) / 2;
inlineTextbox->InternalMove(item->element->width - style->insets.r - style->insets.l, height,
item->element->offsetX + style->insets.l, item->element->offsetY + offset);
} else {
int textboxInset = inlineTextbox->style->insets.l;
int offset = style->metrics->iconSize + style->gapMinor
+ style->insets.l - textboxInset;
int height = inlineTextbox->style->preferredHeight;
inlineTextbox->InternalMove(item->element->width - offset - style->insets.r + textboxInset, height,
item->element->offsetX + offset,
item->element->offsetY + (item->element->height - height) / 2);
}
} else {
inlineTextbox->InternalMove(item->element->width, item->element->height,
item->element->offsetX, item->element->offsetY);
}
}
int ProcessMessage(EsMessage *message) {
int response = scroll.ReceivedMessage(message);
if (response) return response;
if (message->type == ES_MSG_GET_WIDTH || message->type == ES_MSG_GET_HEIGHT) {
if (flags & ES_LIST_VIEW_HORIZONTAL) {
message->measure.width = totalSize + style->insets.l + style->insets.r;
} else {
message->measure.height = totalSize + style->insets.t + style->insets.b;
if (flags & ES_LIST_VIEW_COLUMNS) {
message->measure.width = totalColumnWidth + style->insets.l + style->insets.r;
message->measure.height += columnHeader->style->preferredHeight;
}
}
} else if (message->type == ES_MSG_LAYOUT) {
firstLayout = true;
Wrap(message->layout.sizeChanged);
if (columnHeader) {
columnHeader->InternalMove(Width(GetBounds()), columnHeader->style->preferredHeight, 0, 0);
}
Populate();
} else if (message->type == ES_MSG_SCROLL_X || message->type == ES_MSG_SCROLL_Y) {
int64_t delta = message->scroll.scroll - message->scroll.previous;
if ((message->type == ES_MSG_SCROLL_X) == ((flags & ES_LIST_VIEW_HORIZONTAL) ? true : false)) {
for (uintptr_t i = 0; i < visibleItems.Length(); i++) {
if (flags & ES_LIST_VIEW_HORIZONTAL) visibleItems[i].element->offsetX -= delta;
else visibleItems[i].element->offsetY -= delta;
}
}
useScrollItem = false;
Populate();
Repaint(true);
if (columnHeader && message->type == ES_MSG_SCROLL_X) {
EsElementRelayout(columnHeader);
}
if (selectionBox) {
EsPoint position = EsMouseGetPosition(this);
selectionBoxPositionX = position.x + scroll.position[0];
selectionBoxPositionY = position.y + scroll.position[1];
SelectPreview();
}
} else if (message->type == ES_MSG_DESTROY) {
for (uintptr_t i = 0; i < visibleItems.Length(); i++) {
visibleItems[i].element->Destroy();
}
for (uintptr_t i = 0; i < registeredColumns.Length(); i++) {
if ((registeredColumns[i].flags & ES_LIST_VIEW_COLUMN_DATA_MASK) == ES_LIST_VIEW_COLUMN_DATA_STRINGS) {
for (uintptr_t j = 0; j < registeredColumns[i].items.Length(); j++) {
EsHeapFree(registeredColumns[i].items[j].s.string);
}
}
EsHeapFree(registeredColumns[i].title);
registeredColumns[i].items.Free();
}
EsHeapFree(emptyMessage);
primaryCellStyle->CloseReference();
secondaryCellStyle->CloseReference();
selectedCellStyle->CloseReference();
fixedItems.Free();
fixedItemIndices.Free();
visibleItems.Free();
groups.Free();
activeColumns.Free();
registeredColumns.Free();
if (EsElementIsFocused(this)) {
EsCommandSetCallback(EsCommandByID(instance, ES_COMMAND_SELECT_ALL), nullptr);
}
} else if (message->type == ES_MSG_KEY_UP) {
if (message->keyboard.scancode == ES_SCANCODE_LEFT_CTRL || message->keyboard.scancode == ES_SCANCODE_RIGHT_CTRL) {
SelectPreview();
}
} else if (message->type == ES_MSG_KEY_DOWN) {
if (message->keyboard.scancode == ES_SCANCODE_LEFT_CTRL || message->keyboard.scancode == ES_SCANCODE_RIGHT_CTRL) {
SelectPreview();
}
if (message->keyboard.modifiers & ~(ES_MODIFIER_CTRL | ES_MODIFIER_ALT | ES_MODIFIER_SHIFT)) {
// Unused modifier.
return 0;
}
return KeyInput(message->keyboard.scancode,
message->keyboard.modifiers & ES_MODIFIER_CTRL,
message->keyboard.modifiers & ES_MODIFIER_ALT,
message->keyboard.modifiers & ES_MODIFIER_SHIFT)
? ES_HANDLED : 0;
} else if (message->type == ES_MSG_FOCUSED_START) {
if (!hasFocusedItem && groups.Length() && (message->focus.flags & ES_ELEMENT_FOCUS_FROM_KEYBOARD)) {
hasFocusedItem = true;
focusedItemGroup = 0;
focusedItemIndex = 0;
}
for (uintptr_t i = 0; i < visibleItems.Length(); i++) {
ListViewItem *item = &visibleItems[i];
item->element->customStyleState |= THEME_STATE_LIST_FOCUSED;
if (hasFocusedItem && focusedItemGroup == item->group && focusedItemIndex == item->index) {
item->element->customStyleState |= THEME_STATE_FOCUSED_ITEM;
}
item->element->MaybeRefreshStyle();
}
EsCommand *command = EsCommandByID(instance, ES_COMMAND_SELECT_ALL);
command->data = this;
EsCommandSetCallback(command, [] (EsInstance *, EsElement *, EsCommand *command) {
EsListView *list = (EsListView *) command->data.p;
if (!list->groups.Length() || !list->totalItemCount) return;
list->SetSelected(0, 0, list->groups.Length() - 1, list->groups.Last().itemCount - 1, true, false);
list->UpdateVisibleItemsSelectionState();
});
EsCommandSetDisabled(command, false);
} else if (message->type == ES_MSG_FOCUSED_END) {
for (uintptr_t i = 0; i < visibleItems.Length(); i++) {
ListViewItem *item = &visibleItems[i];
item->element->customStyleState &= ~(THEME_STATE_LIST_FOCUSED | THEME_STATE_FOCUSED_ITEM);
item->element->MaybeRefreshStyle();
}
// Also done in ES_MSG_DESTROY:
EsCommandSetCallback(EsCommandByID(instance, ES_COMMAND_SELECT_ALL), nullptr);
} else if (message->type == ES_MSG_MOUSE_RIGHT_DOWN) {
// Make sure that right clicking will focus the list.
} else if (message->type == ES_MSG_MOUSE_LEFT_DOWN) {
Select(-1, 0, EsKeyboardIsShiftHeld(), EsKeyboardIsCtrlHeld(), false);
} else if (message->type == ES_MSG_MOUSE_LEFT_DRAG) {
if (selectionBox) {
Repaint(false, ES_RECT_4(selectionBox->offsetX, selectionBox->offsetX + selectionBox->width,
selectionBox->offsetY, selectionBox->offsetY + selectionBox->height));
if (!hasSelectionBoxAnchor) {
hasSelectionBoxAnchor = true;
selectionBoxAnchorX = message->mouseDragged.originalPositionX + scroll.position[0];
selectionBoxAnchorY = message->mouseDragged.originalPositionY + scroll.position[1];
if (gui.lastClickButton == ES_MSG_MOUSE_LEFT_DOWN) {
Select(-1, 0, EsKeyboardIsShiftHeld(), EsKeyboardIsCtrlHeld(), false);
}
}
EsElementSetDisabled(selectionBox, false);
selectionBoxPositionX = message->mouseDragged.newPositionX + scroll.position[0];
selectionBoxPositionY = message->mouseDragged.newPositionY + scroll.position[1];
// Inclusive rectangle.
if (selectionBoxPositionX >= selectionBoxAnchorX) selectionBoxPositionX++;
if (selectionBoxPositionY >= selectionBoxAnchorY) selectionBoxPositionY++;
SelectPreview();
} else if (flags & ES_LIST_VIEW_CHOICE_SELECT) {
DragSelect();
}
} else if (message->type == ES_MSG_MOUSE_LEFT_UP || message->type == ES_MSG_MOUSE_RIGHT_UP) {
if (selectionBox) {
EsElementSetDisabled(selectionBox, true);
if (hasSelectionBoxAnchor) {
hasSelectionBoxAnchor = false;
int64_t x1 = selectionBoxPositionX, x2 = selectionBoxAnchorX,
y1 = selectionBoxPositionY, y2 = selectionBoxAnchorY;
if (x1 > x2) { int64_t temp = x1; x1 = x2; x2 = temp; }
if (y1 > y2) { int64_t temp = y1; y1 = y2; y2 = temp; }
SelectBox(x1, x2, y1, y2, EsKeyboardIsCtrlHeld());
}
for (uintptr_t i = 0; i < visibleItems.Length(); i++) {
EsMessage m = { ES_MSG_LIST_VIEW_IS_SELECTED };
m.selectItem.index = visibleItems[i].index;
m.selectItem.group = visibleItems[i].group;
EsMessageSend(this, &m);
EsElement *item = visibleItems[i].element;
if (m.selectItem.isSelected) {
item->customStyleState |= THEME_STATE_SELECTED;
} else {
item->customStyleState &= ~THEME_STATE_SELECTED;
}
item->MaybeRefreshStyle();
}
}
} else if (message->type == ES_MSG_Z_ORDER) {
uintptr_t index = message->zOrder.index;
if (index < visibleItems.Length()) {
EsAssert(zOrderItems.Length() == visibleItems.Length());
message->zOrder.child = zOrderItems[index];
return ES_HANDLED;
} else {
index -= visibleItems.Length();
}
if (selectionBox) { if (index == 0) return message->zOrder.child = selectionBox, ES_HANDLED; else index--; }
if (columnHeader) { if (index == 0) return message->zOrder.child = columnHeader, ES_HANDLED; else index--; }
if (inlineTextbox) { if (index == 0) return message->zOrder.child = inlineTextbox, ES_HANDLED; else index--; }
message->zOrder.child = nullptr;
} else if (message->type == ES_MSG_PAINT && !totalItemCount && emptyMessageBytes) {
EsDrawTextThemed(message->painter, this, EsPainterBoundsInset(message->painter), emptyMessage, emptyMessageBytes,
ES_STYLE_TEXT_LABEL_SECONDARY, ES_TEXT_H_CENTER | ES_TEXT_V_CENTER | ES_TEXT_WRAP);
} else if (message->type == ES_MSG_ANIMATE) {
if (scroll.dragScrolling && (flags & ES_LIST_VIEW_CHOICE_SELECT)) {
DragSelect();
}
uint64_t currentTime = EsTimeStampMs();
int64_t remainingTime = searchBufferLastKeyTime + GetConstantNumber("listViewSearchBufferTimeout") - currentTime;
if (remainingTime < 0) {
ClearSearchBuffer();
} else {
message->animate.waitMs = remainingTime;
message->animate.complete = false;
}
} else if (message->type == ES_MSG_BEFORE_Z_ORDER) {
EsAssert(!zOrderItems.Length());
intptr_t focused = -1, hovered = -1;
for (uintptr_t i = 0; i < visibleItems.Length(); i++) {
if (hasFocusedItem && visibleItems[i].index == focusedItemIndex && visibleItems[i].group == focusedItemGroup) {
focused = i;
} else if (window->hovered == visibleItems[i].element) {
hovered = i;
} else {
zOrderItems.Add(visibleItems[i].element);
}
}
if (hovered != -1) {
zOrderItems.Add(visibleItems[hovered].element);
}
if (focused != -1) {
zOrderItems.Add(visibleItems[focused].element);
}
} else if (message->type == ES_MSG_AFTER_Z_ORDER) {
zOrderItems.Free();
} else if (message->type == ES_MSG_GET_ACCESS_KEY_HINT_BOUNDS) {
AccessKeysCenterHint(this, message);
} else if (message->type == ES_MSG_UI_SCALE_CHANGED) {
primaryCellStyle->CloseReference();
secondaryCellStyle->CloseReference();
selectedCellStyle->CloseReference();
primaryCellStyle = GetStyle(MakeStyleKey(ES_STYLE_LIST_PRIMARY_CELL, 0), false);
secondaryCellStyle = GetStyle(MakeStyleKey(ES_STYLE_LIST_SECONDARY_CELL, 0), false);
selectedCellStyle = GetStyle(MakeStyleKey(ES_STYLE_LIST_SELECTED_CHOICE_CELL, 0), false);
EsListViewChangeStyles(this, 0, 0, 0, 0, ES_FLAGS_DEFAULT, ES_FLAGS_DEFAULT);
} else if (message->type == ES_MSG_LIST_VIEW_GET_CONTENT && (activeColumns.Length() || (flags & ES_LIST_VIEW_FIXED_ITEMS))) {
uintptr_t index = message->getContent.index;
ListViewFixedItemData data = {};
ListViewColumn *column = &registeredColumns[(flags & ES_LIST_VIEW_COLUMNS) ? activeColumns[message->getContent.activeColumnIndex] : 0];
uint32_t format = column->flags & ES_LIST_VIEW_COLUMN_FORMAT_MASK;
uint32_t type = column->flags & ES_LIST_VIEW_COLUMN_DATA_MASK;
if (flags & ES_LIST_VIEW_FIXED_ITEMS) {
EsAssert(index < fixedItems.Length());
index = fixedItemIndices[index];
ListViewFixedItem *item = &fixedItems[index];
if (index < column->items.Length()) data = column->items[index];
if (!activeColumns.Length() || message->getContent.columnID == registeredColumns[activeColumns[0]].id) {
message->getContent.icon = item->iconID;
}
} else {
EsMessage m = { .type = ES_MSG_LIST_VIEW_GET_ITEM_DATA };
m.getItemData.index = message->getContent.index;
m.getItemData.group = message->getContent.group;
m.getItemData.columnID = message->getContent.columnID;
m.getItemData.activeColumnIndex = message->getContent.activeColumnIndex;
EsMessageSend(this, &m);
if (type == ES_LIST_VIEW_COLUMN_DATA_STRINGS) {
data.s.string = (char *) m.getItemData.s;
data.s.bytes = m.getItemData.sBytes;
} else if (type == ES_LIST_VIEW_COLUMN_DATA_DOUBLES) {
data.d = m.getItemData.d;
} else if (type == ES_LIST_VIEW_COLUMN_DATA_INTEGERS) {
data.i = m.getItemData.i;
}
if (!activeColumns.Length() || message->getContent.columnID == registeredColumns[activeColumns[0]].id) {
message->getContent.icon = m.getItemData.icon;
}
}
#define BOOLEAN_FORMAT(trueString, falseString) \
if (type == ES_LIST_VIEW_COLUMN_DATA_INTEGERS) { \
EsBufferFormat(message->getContent.buffer, "%z", data.i ? interfaceString_ ## trueString : interfaceString_ ## falseString); \
} else { \
EsAssert(false); \
}
#define NUMBER_FORMAT(unitString) \
if (type == ES_LIST_VIEW_COLUMN_DATA_INTEGERS) { \
EsBufferFormat(message->getContent.buffer, "%d%z", data.i, interfaceString_ ## unitString); \
} else if (type == ES_LIST_VIEW_COLUMN_DATA_DOUBLES) { \
EsBufferFormat(message->getContent.buffer, "%F%z", data.d, interfaceString_ ## unitString); \
} else { \
EsAssert(false); \
}
#define UNIT_FORMAT(unitString1, unitString2, unitString3) \
double d = type == ES_LIST_VIEW_COLUMN_DATA_INTEGERS ? data.i : type == ES_LIST_VIEW_COLUMN_DATA_DOUBLES ? data.d : 0; \
if (d < 10000) EsBufferFormat(message->getContent.buffer, "%F%z", d, interfaceString_ ## unitString1); \
else if (d < 10000000) EsBufferFormat(message->getContent.buffer, "%.F%z", 1, d / 1000, interfaceString_ ## unitString2); \
else EsBufferFormat(message->getContent.buffer, "%.F%z", 1, d / 1000000, interfaceString_ ## unitString3);
if (format == ES_LIST_VIEW_COLUMN_FORMAT_DEFAULT) {
if (type == ES_LIST_VIEW_COLUMN_DATA_STRINGS) {
EsBufferFormat(message->getContent.buffer, "%s", data.s.bytes, data.s.string);
} else if (type == ES_LIST_VIEW_COLUMN_DATA_DOUBLES) {
EsBufferFormat(message->getContent.buffer, "%F", data.d);
} else if (type == ES_LIST_VIEW_COLUMN_DATA_INTEGERS) {
EsBufferFormat(message->getContent.buffer, "%d", data.i);
}
} else if (format == ES_LIST_VIEW_COLUMN_FORMAT_BYTES) {
if (type == ES_LIST_VIEW_COLUMN_DATA_INTEGERS) {
EsBufferFormat(message->getContent.buffer, "%D", data.i);
} else {
EsAssert(false);
}
} else if (format == ES_LIST_VIEW_COLUMN_FORMAT_ENUM_STRING) {
if (type == ES_LIST_VIEW_COLUMN_DATA_INTEGERS) {
EsAssert(data.i >= 0 && (uintptr_t) data.i < column->enumStringCount);
EsBufferFormat(message->getContent.buffer, "%s", column->enumStrings[data.i].stringBytes, column->enumStrings[data.i].string);
} else {
EsAssert(false);
}
} else if (format == ES_LIST_VIEW_COLUMN_FORMAT_YES_NO) {
BOOLEAN_FORMAT(CommonBooleanYes, CommonBooleanNo);
} else if (format == ES_LIST_VIEW_COLUMN_FORMAT_ON_OFF) {
BOOLEAN_FORMAT(CommonBooleanOn, CommonBooleanOff);
} else if (format == ES_LIST_VIEW_COLUMN_FORMAT_PERCENTAGE) {
NUMBER_FORMAT(CommonUnitPercent);
} else if (format == ES_LIST_VIEW_COLUMN_FORMAT_BITS) {
NUMBER_FORMAT(CommonUnitBits);
} else if (format == ES_LIST_VIEW_COLUMN_FORMAT_PIXELS) {
NUMBER_FORMAT(CommonUnitPixels);
} else if (format == ES_LIST_VIEW_COLUMN_FORMAT_DPI) {
NUMBER_FORMAT(CommonUnitDPI);
} else if (format == ES_LIST_VIEW_COLUMN_FORMAT_SECONDS) {
NUMBER_FORMAT(CommonUnitSeconds);
} else if (format == ES_LIST_VIEW_COLUMN_FORMAT_HERTZ) {
UNIT_FORMAT(CommonUnitHz, CommonUnitKHz, CommonUnitMHz);
} else if (format == ES_LIST_VIEW_COLUMN_FORMAT_BYTE_RATE) {
UNIT_FORMAT(CommonUnitBps, CommonUnitKBps, CommonUnitMBps);
} else {
EsAssert(false);
}
#undef NUMBER_FORMAT
#undef BOOLEAN_FORMAT
} else if (message->type == ES_MSG_LIST_VIEW_IS_SELECTED && (flags & ES_LIST_VIEW_FIXED_ITEMS)) {
message->selectItem.isSelected = message->selectItem.index == fixedItemSelection;
} else if (message->type == ES_MSG_LIST_VIEW_COLUMN_MENU && (flags & ES_LIST_VIEW_FIXED_ITEMS)) {
EsMenu *menu = EsMenuCreate(message->columnMenu.source);
menu->userData = this;
ListViewColumn *column = &registeredColumns[activeColumns[message->columnMenu.activeColumnIndex]];
uint32_t sortMode = column->flags & ES_LIST_VIEW_COLUMN_SORT_MASK;
uint64_t checkAscending = (fixedItemSortDirection == LIST_SORT_DIRECTION_ASCENDING && column->id == fixedItemSortColumnID) ? ES_MENU_ITEM_CHECKED : 0;
uint64_t checkDescending = (fixedItemSortDirection == LIST_SORT_DIRECTION_DESCENDING && column->id == fixedItemSortColumnID) ? ES_MENU_ITEM_CHECKED : 0;
if (sortMode != ES_LIST_VIEW_COLUMN_SORT_NONE) {
EsMenuAddItem(menu, ES_MENU_ITEM_HEADER, INTERFACE_STRING(CommonSortHeader));
if (sortMode == ES_LIST_VIEW_COLUMN_SORT_DEFAULT) {
EsMenuAddItem(menu, checkAscending, INTERFACE_STRING(CommonSortAToZ), ListViewSetSortAscending, column->id);
EsMenuAddItem(menu, checkDescending, INTERFACE_STRING(CommonSortZToA), ListViewSetSortDescending, column->id);
} else if (sortMode == ES_LIST_VIEW_COLUMN_SORT_TIME) {
EsMenuAddItem(menu, checkAscending, INTERFACE_STRING(CommonSortOldToNew), ListViewSetSortAscending, column->id);
EsMenuAddItem(menu, checkDescending, INTERFACE_STRING(CommonSortNewToOld), ListViewSetSortDescending, column->id);
} else if (sortMode == ES_LIST_VIEW_COLUMN_SORT_SIZE) {
EsMenuAddItem(menu, checkAscending, INTERFACE_STRING(CommonSortSmallToLarge), ListViewSetSortAscending, column->id);
EsMenuAddItem(menu, checkDescending, INTERFACE_STRING(CommonSortLargeToSmall), ListViewSetSortDescending, column->id);
}
}
EsMenuShow(menu);
} else {
return 0;
}
return ES_HANDLED;
}
};
void ListViewPopulateActionCallback(EsElement *element, EsGeneric) {
EsListView *view = (EsListView *) element;
EsAssert(view->populateQueued);
view->populateQueued = false;
view->_Populate();
EsAssert(!view->populateQueued);
#if 0
if (element->flags & ES_ELEMENT_DEBUG) {
EsPrint("Populate complete for list %x with scroll %i:\n", view, view->scroll.position[1]);
for (uintptr_t i = 0; i < view->visibleItems.Length(); i++) {
EsMessage m = { ES_MSG_LIST_VIEW_GET_CONTENT };
uint8_t _buffer[512];
EsBuffer buffer = { .out = _buffer, .bytes = sizeof(_buffer) };
m.getContent.buffer = &buffer;
m.getContent.index = view->visibleItems[i].index;
m.getContent.group = view->visibleItems[i].group;
EsMessageSend(view, &m);
EsPrint("\t%d: %d '%s' at %i\n", i, view->visibleItems[i].index, buffer.position, _buffer,
view->visibleItems[i].element->offsetY - view->GetListBounds().t);
}
}
#endif
}
void ListViewEnsureVisibleActionCallback(EsElement *element, EsGeneric) {
EsListView *view = (EsListView *) element;
EsAssert(view->ensureVisibleQueued);
view->ensureVisibleQueued = false;
view->_EnsureItemVisible(view->ensureVisibleGroupIndex, view->ensureVisibleIndex, view->ensureVisibleFlags);
EsAssert(!view->ensureVisibleQueued);
}
int ListViewProcessMessage(EsElement *element, EsMessage *message) {
return ((EsListView *) element)->ProcessMessage(message);
}
int ListViewProcessItemMessage(EsElement *_element, EsMessage *message) {
ListViewItemElement *element = (ListViewItemElement *) _element;
return ((EsListView *) element->parent)->ProcessItemMessage(element->index, message, element);
}
void ListViewCalculateTotalColumnWidth(EsListView *view) {
view->totalColumnWidth = -view->secondaryCellStyle->gapMajor;
for (uintptr_t i = 0; i < view->activeColumns.Length(); i++) {
view->totalColumnWidth += view->registeredColumns[view->activeColumns[i]].width * theming.scale + view->secondaryCellStyle->gapMajor;
}
}
int ListViewColumnHeaderMessage(EsElement *element, EsMessage *message) {
EsListView *view = (EsListView *) element->userData.p;
if (message->type == ES_MSG_LAYOUT) {
int x = view->style->insets.l - view->scroll.position[0];
for (uintptr_t i = 0; i < element->children.Length(); i += 2) {
EsElement *item = element->children[i], *splitter = element->children[i + 1];
ListViewColumn *column = &view->registeredColumns[item->userData.u];
int splitterLeft = splitter->style->preferredWidth - view->secondaryCellStyle->gapMajor;
item->InternalMove(column->width * theming.scale - splitterLeft, element->height, x, 0);
splitter->InternalMove(splitter->style->preferredWidth, element->height, x + column->width * theming.scale - splitterLeft, 0);
x += column->width * theming.scale + view->secondaryCellStyle->gapMajor;
}
} else if (message->type == ES_MSG_MOUSE_LEFT_DOWN || message->type == ES_MSG_MOUSE_RIGHT_DOWN) {
return ES_HANDLED;
}
return 0;
}
void EsListViewChangeStyles(EsListView *view, EsStyleID style, EsStyleID itemStyle,
EsStyleID headerItemStyle, EsStyleID footerItemStyle, uint32_t addFlags, uint32_t removeFlags) {
// TODO Animating changes.
bool wasTiledView = view->flags & ES_LIST_VIEW_TILED;
EsAssert(!(addFlags & removeFlags));
view->flags |= addFlags;
view->flags &= ~(uint64_t) removeFlags;
bool horizontal = view->flags & ES_LIST_VIEW_HORIZONTAL;
if (style) view->SetStyle(style, true);
if (itemStyle) view->itemStyle = itemStyle;
if (headerItemStyle) view->headerItemStyle = headerItemStyle;
if (footerItemStyle) view->footerItemStyle = footerItemStyle;
GetPreferredSizeFromStylePart(view->itemStyle, &view->fixedWidth, &view->fixedHeight);
GetPreferredSizeFromStylePart(view->headerItemStyle, horizontal ? &view->fixedHeaderSize : nullptr, horizontal ? nullptr : &view->fixedHeaderSize);
GetPreferredSizeFromStylePart(view->footerItemStyle, horizontal ? &view->fixedFooterSize : nullptr, horizontal ? nullptr : &view->fixedFooterSize);
if ((view->flags & ES_LIST_VIEW_MULTI_SELECT) && !view->selectionBox) {
view->selectionBox = EsCustomElementCreate(view, ES_CELL_FILL | ES_ELEMENT_DISABLED | ES_ELEMENT_NO_HOVER, ES_STYLE_LIST_SELECTION_BOX);
view->selectionBox->cName = "selection box";
} else if ((~view->flags & ES_LIST_VIEW_MULTI_SELECT) && view->selectionBox) {
EsElementDestroy(view->selectionBox);
view->selectionBox = nullptr;
}
if ((view->flags & ES_LIST_VIEW_COLUMNS) && !view->columnHeader) {
view->columnHeader = EsCustomElementCreate(view, ES_CELL_FILL, ES_STYLE_LIST_COLUMN_HEADER);
view->columnHeader->cName = "column header";
view->columnHeader->userData = view;
view->columnHeader->messageUser = ListViewColumnHeaderMessage;
view->scroll.fixedViewport[1] = view->columnHeader->style->preferredHeight;
} else if ((~view->flags & ES_LIST_VIEW_COLUMNS) && view->columnHeader) {
EsElementDestroy(view->columnHeader);
view->activeColumns.Free();
view->columnHeader = nullptr;
view->scroll.fixedViewport[1] = 0;
}
// It's safe to use ES_SCROLL_MODE_AUTO even in tiled mode,
// because decreasing the secondary axis can only increase the primary axis.
uint8_t scrollXMode = 0, scrollYMode = 0;
if (view->flags & ES_LIST_VIEW_COLUMNS) {
scrollXMode = ES_SCROLL_MODE_AUTO;
scrollYMode = ES_SCROLL_MODE_AUTO;
} else if (view->flags & ES_LIST_VIEW_HORIZONTAL) {
scrollXMode = ES_SCROLL_MODE_AUTO;
} else {
scrollYMode = ES_SCROLL_MODE_AUTO;
}
view->scroll.Setup(view, scrollXMode, scrollYMode, ES_SCROLL_X_DRAG | ES_SCROLL_Y_DRAG);
ListViewCalculateTotalColumnWidth(view);
// Remove existing visible items; the list will need to be repopulated.
for (uintptr_t i = view->visibleItems.Length(); i > 0; i--) {
view->visibleItems[i - 1].element->Destroy();
}
view->visibleItems.SetLength(0);
// Remeasure each group.
if (wasTiledView) {
view->totalSize = 0;
for (uintptr_t i = 0; i < view->groups.Length(); i++) {
view->groups[i].totalSize = 0;
}
}
int64_t spaceDelta = 0;
for (uintptr_t i = 0; i < view->groups.Length(); i++) {
if (!view->groups[i].itemCount) continue;
spaceDelta -= view->groups[i].totalSize;
view->groups[i].totalSize = view->MeasureItems(i, 0, view->groups[i].itemCount);
view->groups[i].totalSize += view->style->gapMinor * (view->groups[i].itemCount - 1);
spaceDelta += view->groups[i].totalSize;
}
view->InsertSpace(spaceDelta, 0);
EsElementRelayout(view);
}
EsListView *EsListViewCreate(EsElement *parent, uint64_t flags, EsStyleID style,
EsStyleID itemStyle, EsStyleID headerItemStyle, EsStyleID footerItemStyle) {
EsListView *view = (EsListView *) EsHeapAllocate(sizeof(EsListView), true);
if (!view) return nullptr;
view->primaryCellStyle = GetStyle(MakeStyleKey(ES_STYLE_LIST_PRIMARY_CELL, 0), false);
view->secondaryCellStyle = GetStyle(MakeStyleKey(ES_STYLE_LIST_SECONDARY_CELL, 0), false);
view->selectedCellStyle = GetStyle(MakeStyleKey(ES_STYLE_LIST_SELECTED_CHOICE_CELL, 0), false); // Only used for choice list views.
view->Initialise(parent, flags | ES_ELEMENT_FOCUSABLE, ListViewProcessMessage, style ?: ES_STYLE_LIST_VIEW);
view->cName = "list view";
view->fixedItemSelection = -1;
view->maximumItemsPerBand = INT_MAX;
if (!itemStyle) {
if (flags & ES_LIST_VIEW_CHOICE_SELECT) itemStyle = ES_STYLE_LIST_CHOICE_ITEM;
else if (flags & ES_LIST_VIEW_TILED) itemStyle = ES_STYLE_LIST_ITEM_TILE;
else itemStyle = ES_STYLE_LIST_ITEM;
}
EsListViewChangeStyles(view, 0, itemStyle, headerItemStyle ?: ES_STYLE_LIST_ITEM_GROUP_HEADER,
footerItemStyle ?: ES_STYLE_LIST_ITEM_GROUP_FOOTER, ES_FLAGS_DEFAULT, ES_FLAGS_DEFAULT);
return view;
}
void EsListViewInsertGroup(EsListView *view, EsListViewIndex group, uint32_t flags) {
EsMessageMutexCheck();
// Add the group.
ListViewGroup empty = { .flags = flags };
EsAssert(group <= (EsListViewIndex) view->groups.Length()); // Invalid group index.
if (!view->groups.Insert(empty, group)) {
return;
}
// Update the group index on visible items.
uintptr_t firstVisibleItemToMove = view->visibleItems.Length();
for (uintptr_t i = 0; i < view->visibleItems.Length(); i++) {
ListViewItem *item = &view->visibleItems[i];
if (item->group >= group) {
item->group++;
if (i < firstVisibleItemToMove) {
firstVisibleItemToMove = i;
}
}
}
// Insert gap between groups.
view->InsertSpace(view->groups.Length() > 1 ? view->style->gapMajor : 0, firstVisibleItemToMove);
// Create header and footer items.
int64_t additionalItems = ((flags & ES_LIST_VIEW_GROUP_HAS_HEADER) ? 1 : 0) + ((flags & ES_LIST_VIEW_GROUP_HAS_FOOTER) ? 1 : 0);
EsListViewInsert(view, group, 0, additionalItems);
view->groups[group].initialised = true;
}
void EsListViewInsert(EsListView *view, EsListViewIndex groupIndex, EsListViewIndex firstIndex, EsListViewIndex count) {
EsMessageMutexCheck();
if (!count) return;
EsAssert(count > 0);
// Get the group.
EsAssert(groupIndex < (EsListViewIndex) view->groups.Length()); // Invalid group index.
ListViewGroup *group = &view->groups[groupIndex];
if (group->initialised) {
if (group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) {
EsAssert(firstIndex > 0); // Cannot insert before group header.
}
if (group->flags & ES_LIST_VIEW_GROUP_HAS_FOOTER) {
EsAssert(firstIndex < (intptr_t) group->itemCount); // Cannot insert after group footer.
}
}
// Add the items to the group.
bool addedFirstItemInGroup = !group->itemCount;
group->itemCount += count;
int64_t totalSizeOfItems = view->MeasureItems(groupIndex, firstIndex, count);
int64_t sizeToAdd = (count - (addedFirstItemInGroup ? 1 : 0)) * view->style->gapMinor + totalSizeOfItems;
group->totalSize += sizeToAdd;
view->totalItemCount += count;
// Update indices of visible items.
uintptr_t firstVisibleItemToMove = view->visibleItems.Length();
if (view->hasFocusedItem && view->focusedItemGroup == groupIndex) {
if (view->focusedItemIndex >= firstIndex) {
view->focusedItemIndex += count;
}
}
if (view->hasAnchorItem && view->anchorItemGroup == groupIndex) {
if (view->anchorItemIndex >= firstIndex) {
view->anchorItemIndex += count;
}
}
for (uintptr_t i = 0; i < view->visibleItems.Length(); i++) {
ListViewItem *item = &view->visibleItems[i];
if (item->group < groupIndex) {
continue;
} else if (item->group > groupIndex) {
if (i < firstVisibleItemToMove) {
firstVisibleItemToMove = i;
}
break;
}
if (item->index >= firstIndex) {
item->index += count;
if (i < firstVisibleItemToMove) {
firstVisibleItemToMove = i;
}
}
}
// Insert the space for the items.
view->InsertSpace(sizeToAdd, firstVisibleItemToMove);
}
void EsListViewRemove(EsListView *view, EsListViewIndex groupIndex, EsListViewIndex firstIndex, EsListViewIndex count) {
EsMessageMutexCheck();
if (!count) return;
EsAssert(count > 0);
// Get the group.
EsAssert(groupIndex < (EsListViewIndex) view->groups.Length()); // Invalid group index.
ListViewGroup *group = &view->groups[groupIndex];
if (group->initialised) {
if (group->flags & ES_LIST_VIEW_GROUP_HAS_HEADER) {
EsAssert(firstIndex > 0); // Cannot remove the group header.
}
if (group->flags & ES_LIST_VIEW_GROUP_HAS_FOOTER) {
EsAssert(firstIndex + count < (intptr_t) group->itemCount); // Cannot remove the group footer.
}
}
// Remove the items from the group.
int64_t totalSizeOfItems = view->MeasureItems(groupIndex, firstIndex, count);
int64_t sizeToRemove = (int64_t) group->itemCount == count ? group->totalSize
: (count * view->style->gapMinor + totalSizeOfItems);
group->itemCount -= count;
group->totalSize -= sizeToRemove;
view->totalItemCount -= count;
// Update indices of visible items,
// and remove deleted items.
uintptr_t firstVisibleItemToMove = view->visibleItems.Length();
if (view->hasFocusedItem && view->focusedItemGroup == groupIndex) {
if (view->focusedItemIndex >= firstIndex && view->focusedItemIndex < firstIndex + count) {
view->hasFocusedItem = false;
} else {
view->focusedItemIndex -= count;
}
}
if (view->hasAnchorItem && view->anchorItemGroup == groupIndex) {
if (view->focusedItemIndex >= firstIndex && view->anchorItemIndex < firstIndex + count) {
view->hasAnchorItem = false;
} else {
view->anchorItemIndex -= count;
}
}
for (uintptr_t i = 0; i < view->visibleItems.Length(); i++) {
ListViewItem *item = &view->visibleItems[i];
if (item->group < groupIndex) {
continue;
} else if (item->group > groupIndex) {
if (i < firstVisibleItemToMove) {
firstVisibleItemToMove = i;
}
break;
}
if (item->index >= firstIndex + count) {
item->index -= count;
if (i < firstVisibleItemToMove) {
firstVisibleItemToMove = i;
}
} else if (item->index >= firstIndex && item->index < firstIndex + count) {
item->element->index = i;
item->element->Destroy();
view->visibleItems.Delete(i);
i--;
}
}
// Remove the space of the items.
view->InsertSpace(-sizeToRemove, firstVisibleItemToMove);
}
void EsListViewRemoveAll(EsListView *view, EsListViewIndex group) {
EsMessageMutexCheck();
if (view->groups[group].itemCount) {
EsListViewRemove(view, group, 0, view->groups[group].itemCount);
}
}
int ListViewColumnHeaderItemMessage(EsElement *element, EsMessage *message) {
EsListView *view = (EsListView *) element->parent->parent;
if (message->type == ES_MSG_DESTROY) {
return 0;
}
ListViewColumn *column = &view->registeredColumns[element->userData.u];
if (message->type == ES_MSG_PAINT) {
EsMessage m = { ES_MSG_LIST_VIEW_GET_COLUMN_SORT };
m.getColumnSort.index = element->userData.u;
int sort = EsMessageSend(view, &m);
EsDrawContent(message->painter, element, element->GetBounds(),
column->title, column->titleBytes, 0,
sort == ES_LIST_VIEW_COLUMN_SORT_ASCENDING ? ES_DRAW_CONTENT_MARKER_UP_ARROW
: sort == ES_LIST_VIEW_COLUMN_SORT_DESCENDING ? ES_DRAW_CONTENT_MARKER_DOWN_ARROW : ES_FLAGS_DEFAULT);
} else if (message->type == ES_MSG_MOUSE_LEFT_CLICK && (column->flags & ES_LIST_VIEW_COLUMN_HAS_MENU)) {
EsMessage m = { ES_MSG_LIST_VIEW_COLUMN_MENU };
m.columnMenu.source = element;
m.columnMenu.activeColumnIndex = element->userData.u;
m.columnMenu.columnID = view->registeredColumns[element->userData.u].id;
EsMessageSend(view, &m);
} else if (message->type == ES_MSG_MOUSE_LEFT_DOWN) {
} else {
return 0;
}
return ES_HANDLED;
}
int ListViewColumnSplitterMessage(EsElement *element, EsMessage *message) {
EsListView *view = (EsListView *) element->parent->parent;
if (message->type == ES_MSG_DESTROY) {
return 0;
}
ListViewColumn *column = &view->registeredColumns[element->userData.u];
if (message->type == ES_MSG_MOUSE_LEFT_DOWN) {
view->columnResizingOriginalWidth = column->width * theming.scale;
} else if (message->type == ES_MSG_MOUSE_LEFT_DRAG) {
int width = message->mouseDragged.newPositionX - message->mouseDragged.originalPositionX + view->columnResizingOriginalWidth;
int minimumWidth = element->style->metrics->minimumWidth;
if (width < minimumWidth) width = minimumWidth;
column->width = width / theming.scale;
ListViewCalculateTotalColumnWidth(view);
EsElementRelayout(element->parent);
EsElementRelayout(view);
} else {
return 0;
}
return ES_HANDLED;
}
void EsListViewAddAllColumns(EsListView *view) {
EsElementDestroyContents(view->columnHeader);
EsAssert(view->flags & ES_LIST_VIEW_COLUMNS); // List view does not have columns flag set.
view->activeColumns.Free();
for (uintptr_t i = 0; i < view->registeredColumns.Length(); i++) {
view->activeColumns.Add(i);
EsStyleID style = (view->registeredColumns[i].flags & ES_LIST_VIEW_COLUMN_HAS_MENU) ? ES_STYLE_LIST_COLUMN_HEADER_ITEM_HAS_MENU : ES_STYLE_LIST_COLUMN_HEADER_ITEM;
EsElement *columnHeaderItem = EsCustomElementCreate(view->columnHeader, ES_CELL_FILL, style);
columnHeaderItem->messageUser = ListViewColumnHeaderItemMessage;
columnHeaderItem->cName = "column header item";
columnHeaderItem->userData = i;
EsElement *splitter = EsCustomElementCreate(view->columnHeader, ES_CELL_FILL, ES_STYLE_LIST_COLUMN_HEADER_SPLITTER);
splitter->messageUser = ListViewColumnSplitterMessage;
splitter->cName = "column header splitter";
splitter->userData = i;
}
ListViewCalculateTotalColumnWidth(view);
view->scroll.Refresh();
}
void EsListViewRegisterColumn(EsListView *view, uint32_t id, const char *title, ptrdiff_t titleBytes, uint32_t flags, double initialWidth) {
EsMessageMutexCheck();
if (!initialWidth) {
initialWidth = (view->registeredColumns.Length() ? view->secondaryCellStyle : view->primaryCellStyle)->preferredWidth / theming.scale;
}
ListViewColumn column = {};
column.id = id;
column.flags = flags;
column.width = initialWidth;
if (titleBytes == -1) titleBytes = EsCStringLength(title);
HeapDuplicate((void **) &column.title, &column.titleBytes, title, titleBytes);
view->registeredColumns.Add(column);
}
void EsListViewContentChanged(EsListView *view) {
EsMessageMutexCheck();
view->searchBufferLastKeyTime = 0;
view->searchBufferBytes = 0;
view->scroll.SetX(0);
view->scroll.SetY(0);
view->hasScrollItem = false;
view->useScrollItem = false;
EsListViewInvalidateAll(view);
}
void EsListViewFocusItem(EsListView *view, EsListViewIndex group, EsListViewIndex index) {
ListViewItem *oldFocus = view->FindVisibleItem(view->focusedItemGroup, view->focusedItemIndex);
if (oldFocus) {
oldFocus->element->customStyleState &= ~THEME_STATE_FOCUSED_ITEM;
oldFocus->element->MaybeRefreshStyle();
}
view->hasFocusedItem = true;
view->focusedItemGroup = group;
view->focusedItemIndex = index;
ListViewItem *newFocus = view->FindVisibleItem(view->focusedItemGroup, view->focusedItemIndex);
if (newFocus) {
newFocus->element->customStyleState |= THEME_STATE_FOCUSED_ITEM;
newFocus->element->MaybeRefreshStyle();
}
view->EnsureItemVisible(group, index, ES_FLAGS_DEFAULT);
}
bool EsListViewGetFocusedItem(EsListView *view, EsListViewIndex *group, EsListViewIndex *index) {
if (view->hasFocusedItem) {
if (group) *group = view->focusedItemGroup;
if (index) *index = view->focusedItemIndex;
}
return view->hasFocusedItem;
}
void EsListViewSelectNone(EsListView *view) {
EsMessageMutexCheck();
view->Select(-1, 0, false, false, false);
}
void EsListViewSelect(EsListView *view, EsListViewIndex group, EsListViewIndex index, bool addToExistingSelection) {
EsMessageMutexCheck();
if (addToExistingSelection) {
view->SetSelected(group, index, group, index, true, false);
view->UpdateVisibleItemsSelectionState();
} else {
view->Select(group, index, false, false, false);
}
}
void EsListViewSetEmptyMessage(EsListView *view, const char *message, ptrdiff_t messageBytes) {
EsMessageMutexCheck();
if (messageBytes == -1) messageBytes = EsCStringLength(message);
HeapDuplicate((void **) &view->emptyMessage, &view->emptyMessageBytes, message, messageBytes);
if (!view->totalItemCount) {
view->Repaint(true);
}
}
EsListViewIndex EsListViewGetIndexFromItem(EsElement *_element, EsListViewIndex *group) {
ListViewItemElement *element = (ListViewItemElement *) _element;
EsListView *view = (EsListView *) element->parent;
EsAssert(element->index < view->visibleItems.Length());
if (group) *group = view->visibleItems[element->index].group;
return view->visibleItems[element->index].index;
}
void EsListViewInvalidateAll(EsListView *view) {
view->UpdateVisibleItemsSelectionState();
view->Repaint(true);
}
void EsListViewInvalidateContent(EsListView *view, EsListViewIndex group, EsListViewIndex index) {
for (uintptr_t i = 0; i < view->visibleItems.Length(); i++) {
if (view->visibleItems[i].group == group && view->visibleItems[i].index == index) {
view->UpdateVisibleItemSelectionState(i);
view->visibleItems[i].element->Repaint(true);
break;
}
}
}
#define LIST_VIEW_SORT_FUNCTION(_name, _line) \
ES_MACRO_SORT(_name, EsListViewIndex, { \
ListViewFixedItemData *left = (ListViewFixedItemData *) &context->items[*_left]; \
ListViewFixedItemData *right = (ListViewFixedItemData *) &context->items[*_right]; \
result = _line; \
}, ListViewColumn *)
LIST_VIEW_SORT_FUNCTION(ListViewSortByStringsAscending, EsStringCompare(left->s.string, left->s.bytes, right->s.string, right->s.bytes));
LIST_VIEW_SORT_FUNCTION(ListViewSortByStringsDescending, -EsStringCompare(left->s.string, left->s.bytes, right->s.string, right->s.bytes));
LIST_VIEW_SORT_FUNCTION(ListViewSortByEnumsAscending, EsStringCompare(context->enumStrings[left->i].string, context->enumStrings[left->i].stringBytes,
context->enumStrings[right->i].string, context->enumStrings[right->i].stringBytes));
LIST_VIEW_SORT_FUNCTION(ListViewSortByEnumsDescending, -EsStringCompare(context->enumStrings[left->i].string, context->enumStrings[left->i].stringBytes,
context->enumStrings[right->i].string, context->enumStrings[right->i].stringBytes));
LIST_VIEW_SORT_FUNCTION(ListViewSortByIntegersAscending, left->i > right->i ? 1 : left->i == right->i ? 0 : -1);
LIST_VIEW_SORT_FUNCTION(ListViewSortByIntegersDescending, left->i < right->i ? 1 : left->i == right->i ? 0 : -1);
LIST_VIEW_SORT_FUNCTION(ListViewSortByDoublesAscending, left->d > right->d ? 1 : left->d == right->d ? 0 : -1);
LIST_VIEW_SORT_FUNCTION(ListViewSortByDoublesDescending, left->d < right->d ? 1 : left->d == right->d ? 0 : -1);
ListViewSortFunction ListViewGetSortFunction(ListViewColumn *column, uint8_t direction) {
if ((column->flags & ES_LIST_VIEW_COLUMN_DATA_MASK) == ES_LIST_VIEW_COLUMN_DATA_STRINGS) {
return (direction == LIST_SORT_DIRECTION_DESCENDING ? ListViewSortByStringsDescending : ListViewSortByStringsAscending);
} else if ((column->flags & ES_LIST_VIEW_COLUMN_DATA_MASK) == ES_LIST_VIEW_COLUMN_DATA_INTEGERS) {
if ((column->flags & ES_LIST_VIEW_COLUMN_FORMAT_MASK) == ES_LIST_VIEW_COLUMN_FORMAT_ENUM_STRING) {
return (direction == LIST_SORT_DIRECTION_DESCENDING ? ListViewSortByEnumsDescending : ListViewSortByEnumsAscending);
} else {
return (direction == LIST_SORT_DIRECTION_DESCENDING ? ListViewSortByIntegersDescending : ListViewSortByIntegersAscending);
}
} else if ((column->flags & ES_LIST_VIEW_COLUMN_DATA_MASK) == ES_LIST_VIEW_COLUMN_DATA_DOUBLES) {
return (direction == LIST_SORT_DIRECTION_DESCENDING ? ListViewSortByDoublesDescending : ListViewSortByDoublesAscending);
} else {
EsAssert(false);
}
return nullptr;
}
EsListViewIndex EsListViewFixedItemInsert(EsListView *view, EsGeneric data, EsListViewIndex index, uint32_t iconID) {
EsAssert(view->flags & ES_LIST_VIEW_FIXED_ITEMS);
if (!view->groups.Length()) {
EsListViewInsertGroup(view, 0, ES_FLAGS_DEFAULT);
}
if (!view->registeredColumns.Length()) {
EsListViewRegisterColumn(view, 0, nullptr, 0);
}
if (index == -1) {
index = view->fixedItems.Length();
}
EsAssert(index >= 0 && index <= (intptr_t) view->fixedItems.Length());
ListViewFixedItem item = {};
item.data = data;
item.iconID = iconID;
view->fixedItems.Insert(item, index);
view->fixedItemIndices.Insert(index, index);
ListViewFixedItemData emptyData = {};
for (uintptr_t i = 0; i < view->registeredColumns.Length(); i++) {
ListViewColumn *column = &view->registeredColumns[i];
if (column->items.Length() >= (uintptr_t) index) {
column->items.InsertPointer(&emptyData, index);
}
}
EsListViewInsert(view, 0, index, 1);
return index;
}
void ListViewFixedItemSetInternal(EsListView *view, EsListViewIndex index, uint32_t columnID, ListViewFixedItemData data, uint32_t dataType) {
EsAssert(view->flags & ES_LIST_VIEW_FIXED_ITEMS);
EsMessageMutexCheck();
EsAssert(index >= 0 && index < (intptr_t) view->fixedItems.Length());
ListViewColumn *column = nullptr;
for (uintptr_t i = 0; i < view->registeredColumns.Length(); i++) {
if (view->registeredColumns[i].id == columnID) {
column = &view->registeredColumns[i];
break;
}
}
EsAssert(column);
EsAssert((column->flags & ES_LIST_VIEW_COLUMN_DATA_MASK) == dataType);
// Make sure that the column's array of items has been updated to match to the size of fixedItems.
if (column->items.Length() < view->fixedItems.Length()) {
uintptr_t oldLength = column->items.Length();
column->items.SetLength(view->fixedItems.Length());
EsMemoryZero(&column->items[oldLength], (view->fixedItems.Length() - oldLength) * sizeof(column->items[0]));
}
bool changed = false;
if (dataType == ES_LIST_VIEW_COLUMN_DATA_STRINGS) {
changed = EsStringCompareRaw(column->items[index].s.string, column->items[index].s.bytes, data.s.string, data.s.bytes);
} else if (dataType == ES_LIST_VIEW_COLUMN_DATA_DOUBLES) {
changed = column->items[index].d != data.d;
} else if (dataType == ES_LIST_VIEW_COLUMN_DATA_INTEGERS) {
changed = column->items[index].i != data.i;
} else {
EsAssert(false);
}
if (dataType == ES_LIST_VIEW_COLUMN_DATA_STRINGS) {
EsHeapFree(column->items[index].s.string);
}
column->items[index] = data;
if (changed) {
for (uintptr_t i = 0; i < view->fixedItemIndices.Length(); i++) {
if (view->fixedItemIndices[i] == index) {
EsListViewInvalidateContent(view, 0, i);
break;
}
}
}
}
void EsListViewFixedItemSetString(EsListView *view, EsListViewIndex index, uint32_t columnID, const char *string, ptrdiff_t stringBytes) {
ListViewFixedString fixedString = {};
fixedString.bytes = stringBytes == -1 ? EsCStringLength(string) : stringBytes;
size_t outBytes;
HeapDuplicate((void **) &fixedString, &outBytes, string, fixedString.bytes);
if (outBytes == fixedString.bytes) {
ListViewFixedItemSetInternal(view, index, columnID, { .s = fixedString }, ES_LIST_VIEW_COLUMN_DATA_STRINGS);
}
}
void EsListViewFixedItemSetDouble(EsListView *view, EsListViewIndex index, uint32_t columnID, double number) {
ListViewFixedItemSetInternal(view, index, columnID, { .d = number }, ES_LIST_VIEW_COLUMN_DATA_DOUBLES);
}
void EsListViewFixedItemSetInteger(EsListView *view, EsListViewIndex index, uint32_t columnID, int64_t number) {
ListViewFixedItemSetInternal(view, index, columnID, { .i = number }, ES_LIST_VIEW_COLUMN_DATA_INTEGERS);
}
bool EsListViewFixedItemFindIndex(EsListView *view, EsGeneric data, EsListViewIndex *index) {
EsAssert(view->flags & ES_LIST_VIEW_FIXED_ITEMS);
EsMessageMutexCheck();
for (uintptr_t i = 0; i < view->fixedItemIndices.Length(); i++) {
if (view->fixedItems[view->fixedItemIndices[i]].data == data) {
*index = i;
return true;
}
}
return false;
}
bool EsListViewFixedItemSelect(EsListView *view, EsGeneric data) {
EsAssert(view->flags & ES_LIST_VIEW_FIXED_ITEMS);
EsMessageMutexCheck();
EsListViewIndex index;
bool found = EsListViewFixedItemFindIndex(view, data, &index);
if (found) {
EsListViewSelect(view, 0, index);
// TODO Maybe you should have to separately call EsListViewFocusItem to get this behaviour?
EsListViewFocusItem(view, 0, index);
view->EnsureItemVisible(0, index, ENSURE_VISIBLE_ALIGN_CENTER);
}
return found;
}
bool EsListViewFixedItemRemove(EsListView *view, EsGeneric data) {
EsAssert(view->flags & ES_LIST_VIEW_FIXED_ITEMS);
EsMessageMutexCheck();
EsListViewIndex index;
bool found = EsListViewFixedItemFindIndex(view, data, &index);
if (found) {
EsListViewRemove(view, 0, index, 1);
EsListViewIndex fixedIndex = view->fixedItemIndices[index];
for (uintptr_t i = 0; i < view->registeredColumns.Length(); i++) {
ListViewColumn *column = &view->registeredColumns[i];
if ((uintptr_t) fixedIndex < column->items.Length()) {
if ((column->flags & ES_LIST_VIEW_COLUMN_DATA_MASK) == ES_LIST_VIEW_COLUMN_DATA_STRINGS) {
EsHeapFree(column->items[fixedIndex].s.string);
}
column->items.Delete(fixedIndex);
if (!column->items.Length()) {
column->items.Free();
}
}
}
view->fixedItems.Delete(fixedIndex);
view->fixedItemIndices.Delete(index);
for (uintptr_t i = 0; i < view->fixedItemIndices.Length(); i++) {
if (view->fixedItemIndices[i] > fixedIndex) {
view->fixedItemIndices[i]--;
}
}
}
return found;
}
bool EsListViewFixedItemGetSelected(EsListView *view, EsGeneric *data) {
EsAssert(view->flags & ES_LIST_VIEW_FIXED_ITEMS);
EsMessageMutexCheck();
if (view->fixedItemSelection == -1 || view->fixedItemSelection >= (intptr_t) view->fixedItems.Length()) {
return false;
} else {
*data = view->fixedItems[view->fixedItemIndices[view->fixedItemSelection]].data;
return true;
}
}
void EsListViewFixedItemSetEnumStringsForColumn(EsListView *view, uint32_t columnID, const EsListViewEnumString *strings, size_t stringCount) {
for (uintptr_t i = 0; i < view->registeredColumns.Length(); i++) {
if (view->registeredColumns[i].id == columnID) {
view->registeredColumns[i].enumStrings = strings;
view->registeredColumns[i].enumStringCount = stringCount;
EsListViewInvalidateAll(view);
return;
}
}
EsAssert(false);
}
void EsListViewFixedItemSortAll(EsListView *view) {
ListViewColumn *column = nullptr;
for (uintptr_t i = 0; i < view->registeredColumns.Length(); i++) {
if (view->registeredColumns[i].id == view->fixedItemSortColumnID) {
column = &view->registeredColumns[i];
break;
}
}
EsAssert(column);
EsAssert(view->fixedItems.Length() == view->fixedItemIndices.Length());
EsListViewIndex previousSelectionIndex = view->fixedItemSelection >= 0 && (uintptr_t) view->fixedItemSelection < view->fixedItemIndices.Length()
? view->fixedItemIndices[view->fixedItemSelection] : -1;
ListViewSortFunction sortFunction = ListViewGetSortFunction(column, view->fixedItemSortDirection);
sortFunction(view->fixedItemIndices.array, view->fixedItems.Length(), column);
EsListViewInvalidateAll(view);
if (previousSelectionIndex != -1) {
for (uintptr_t i = 0; i < view->fixedItemIndices.Length(); i++) {
if (view->fixedItemIndices[i] == previousSelectionIndex) {
EsListViewSelect(view, 0, i);
EsListViewFocusItem(view, 0, i);
view->EnsureItemVisible(0, i, ENSURE_VISIBLE_ALIGN_CENTER);
break;
}
}
}
}
void ListViewSetSortDirection(EsListView *view, uint32_t columnID, uint8_t direction) {
if (view->fixedItemSortColumnID != columnID || view->fixedItemSortDirection != direction) {
view->fixedItemSortColumnID = columnID;
view->fixedItemSortDirection = direction;
EsListViewFixedItemSortAll(view);
}
}
void ListViewSetSortAscending(EsMenu *menu, EsGeneric context) {
ListViewSetSortDirection((EsListView *) menu->userData.p, context.u, LIST_SORT_DIRECTION_ASCENDING);
}
void ListViewSetSortDescending(EsMenu *menu, EsGeneric context) {
ListViewSetSortDirection((EsListView *) menu->userData.p, context.u, LIST_SORT_DIRECTION_DESCENDING);
}
int ListViewInlineTextboxMessage(EsElement *element, EsMessage *message) {
int response = ProcessTextboxMessage(element, message);
if (message->type == ES_MSG_DESTROY) {
EsListView *view = (EsListView *) EsElementGetLayoutParent(element);
view->inlineTextbox = nullptr;
ListViewItem *item = view->FindVisibleItem(view->inlineTextboxGroup, view->inlineTextboxIndex);
if (item) EsElementRepaint(item->element);
EsElementFocus(view);
}
return response;
}
EsTextbox *EsListViewCreateInlineTextbox(EsListView *view, EsListViewIndex group, EsListViewIndex index, uint32_t flags) {
if (view->inlineTextbox) {
view->inlineTextbox->Destroy();
}
view->inlineTextboxGroup = group;
view->inlineTextboxIndex = index;
view->EnsureItemVisible(group, index, ENSURE_VISIBLE_ALIGN_TOP);
uint64_t textboxFlags = ES_CELL_FILL | ES_TEXTBOX_EDIT_BASED | ES_TEXTBOX_ALLOW_TABS;
if (flags & ES_LIST_VIEW_INLINE_TEXTBOX_REJECT_EDIT_IF_FOCUS_LOST) {
textboxFlags |= ES_TEXTBOX_REJECT_EDIT_IF_LOST_FOCUS;
}
view->inlineTextbox = EsTextboxCreate(view, textboxFlags, ES_STYLE_TEXTBOX_INLINE);
if (!view->inlineTextbox) {
return nullptr;
}
EsAssert(view->inlineTextbox->messageClass == ProcessTextboxMessage);
view->inlineTextbox->messageClass = ListViewInlineTextboxMessage;
if (flags & ES_LIST_VIEW_INLINE_TEXTBOX_COPY_EXISTING_TEXT) {
EsMessage m = { ES_MSG_LIST_VIEW_GET_CONTENT };
uint8_t _buffer[256];
EsBuffer buffer = { .out = _buffer, .bytes = sizeof(_buffer) };
m.getContent.buffer = &buffer;
m.getContent.index = index;
m.getContent.group = group;
EsMessageSend(view, &m);
EsTextboxInsert(view->inlineTextbox, (char *) _buffer, buffer.position);
EsTextboxSelectAll(view->inlineTextbox);
}
if (view->searchBufferBytes) {
view->searchBufferBytes = 0;
EsElementRepaint(view);
}
EsElementRelayout(view);
EsElementFocus(view->inlineTextbox);
EsTextboxStartEdit(view->inlineTextbox);
return view->inlineTextbox;
}
void EsListViewScrollToEnd(EsListView *view) {
if (view->flags & ES_LIST_VIEW_HORIZONTAL) {
view->scroll.SetX(view->scroll.limit[0]);
} else {
view->scroll.SetY(view->scroll.limit[1]);
}
}
EsListViewEnumeratedVisibleItem *EsListViewEnumerateVisibleItems(EsListView *view, size_t *count) {
EsMessageMutexCheck();
*count = view->visibleItems.Length();
EsListViewEnumeratedVisibleItem *result = (EsListViewEnumeratedVisibleItem *) EsHeapAllocate(sizeof(EsListViewEnumeratedVisibleItem) * *count, true);
if (!result) {
*count = 0;
return nullptr;
}
for (uintptr_t i = 0; i < *count; i++) {
result[i].element = view->visibleItems[i].element;
result[i].group = view->visibleItems[i].group;
result[i].index = view->visibleItems[i].index;
}
return result;
}
void EsListViewSetMaximumItemsPerBand(EsListView *view, int maximumItemsPerBand) {
view->maximumItemsPerBand = maximumItemsPerBand;
}
EsPoint EsListViewGetAnnouncementPointForSelection(EsListView *view) {
EsRectangle viewWindowBounds = EsElementGetWindowBounds(view);
EsRectangle bounding = viewWindowBounds;
bool first = true;
for (uintptr_t i = 0; i < view->visibleItems.Length(); i++) {
if (~view->visibleItems[i].element->customStyleState & THEME_STATE_SELECTED) continue;
EsRectangle bounds = EsElementGetWindowBounds(view->visibleItems[i].element);
if (first) bounding = bounds;
else bounding = EsRectangleBounding(bounding, bounds);
first = false;
}
bounding = EsRectangleIntersection(bounding, viewWindowBounds);
return ES_POINT((bounding.l + bounding.r) / 2, (bounding.t + bounding.b) / 2);
}