Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/core/test/fixtures/gea-elements-virtual-list/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Component } from '@geastack/core'
import { Rows } from './Rows'

export class App extends Component {
template() {
return (
<div class="app">
<Rows />
</div>
)
}
}
27 changes: 27 additions & 0 deletions packages/core/test/fixtures/gea-elements-virtual-list/Rows.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Store, type VirtualListElement } from '@geastack/core'
import { VirtualList } from '@geastack/elements'

// The @geastack/elements VirtualList used the way its README shows: the app
// keeps scrollTop in a Store and copies the element's offset back on scroll.
class RowsStore extends Store {
scrollTop = 0
}

const rows = new RowsStore()

export function Rows() {
return (
<VirtualList
id="rows"
class="rows"
itemCount={100}
itemHeight={40}
scrollTop={rows.scrollTop}
itemTemplate={(index) => <span class="row-label">{'Row ' + index}</span>}
onScroll={() => {
const list = document.getElementById('rows') as VirtualListElement | null
if (list) rows.scrollTop = list.scrollTop
}}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { mount } from '@geastack/core'
import { App } from './App'
import './styles.css'

mount(App)
13 changes: 13 additions & 0 deletions packages/core/test/fixtures/gea-elements-virtual-list/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.app {
width: 100vw;
height: 100vh;
}

.rows {
width: 100vw;
height: 400px;
}

.row-label {
font-size: 16px;
}
9 changes: 9 additions & 0 deletions packages/core/test/run-gea-elements-virtual-list-pipeline.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
exec "$ROOT/packages/core/test/run-gea-native-app-pipeline.sh" \
"gea-elements-virtual-list-pipeline" \
"$ROOT/packages/core/test/fixtures/gea-elements-virtual-list" \
"index.tsx" \
"packages/core/test/test_gea_elements_virtual_list_main.cpp"
95 changes: 95 additions & 0 deletions packages/core/test/test_gea_elements_virtual_list_main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#include "native_test_harness.h"

#include "app.h"
#include "ui/tree_internal.h"

#include <cstdio>
#include <cstdlib>
#include <string>
#include <vector>

extern void __gea_top_level();

namespace {
constexpr int kViewportWidth = 720;
constexpr int kViewportHeight = 1440;
constexpr double kDevicePixelRatio = 2.0;
constexpr const char *kTest = "test_gea_elements_virtual_list_main";
// VirtualList renders a fixed pool of SLOT_COUNT (VirtualList.tsx) slots.
constexpr int kSlotCount = 16;

// The row indexes the visible labels show. nodesWithClass skips hidden nodes,
// so slots past the end of the list are not counted.
std::vector<int> visibleRowIndexes()
{
std::vector<int> out;
for (const int id : gea::embedded::test::nodesWithClass("row-label")) {
const std::string text = gea::embedded::test::textContent(id);
if (text.rfind("Row ", 0) != 0) continue;
out.push_back(std::atoi(text.c_str() + 4));
}
return out;
}

// Every slot shows exactly one row, and together they show rows
// first..first+15 with no gap or repeat.
bool expectRows(const char *phase, int first)
{
const auto indexes = visibleRowIndexes();
std::vector<bool> seen(kSlotCount, false);
bool ok = indexes.size() == static_cast<std::size_t>(kSlotCount);
for (const int index : indexes) {
const int offset = index - first;
if (offset < 0 || offset >= kSlotCount || seen[offset]) ok = false;
else seen[offset] = true;
}
if (!ok) {
std::fprintf(stderr, "[%s] %s: expected rows %d..%d once each, got %zu labels:", kTest, phase, first, first + kSlotCount - 1, indexes.size());
for (const int index : indexes) std::fprintf(stderr, " %d", index);
std::fprintf(stderr, "\n");
}
return ok;
}
} // namespace

int main()
{
using namespace gea::embedded::test;
auto &tree = gea::embedded::ui::Tree::instance();

resetNativeHost();
setNativeDisplaySize(kViewportWidth, kViewportHeight);
gea::framework::app::Application::init(kViewportWidth, kViewportHeight, kDevicePixelRatio);
__gea_top_level();
refresh();
gea::framework::app::Application::frame(16);

const auto lists = nodesWithType(gea::embedded::ui::NodeType::VirtualList);
if (lists.size() != 1) {
std::fprintf(stderr, "[%s] expected one virtual-list node, got %zu\n", kTest, lists.size());
dumpTree(kTest);
return 1;
}
const auto &list = tree.node(lists[0]);

// The rows are absolutely positioned, so the list's height comes only from
// the class the app passes.
const int expectedHeight = static_cast<int>(400 * kDevicePixelRatio);
bool ok = true;
if (list.layout.height != expectedHeight) {
std::fprintf(stderr, "[%s] list height %d, expected %d from .rows\n", kTest, list.layout.height, expectedHeight);
ok = false;
}

// The pool shows the first 16 rows, each from itemTemplate.
ok = ok && expectRows("initial", 0);

// Scrolling is not checked here: a scrollTop change re-renders VirtualList
// and replaces its <virtual-list> node, which resets the native offset.

if (!ok) {
dumpTree(kTest);
return 1;
}
return 0;
}
5 changes: 3 additions & 2 deletions packages/elements/components/VirtualList.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ClassValue, Event, TouchEventHandler } from '@geastack/core'
import type { ClassValue, Event, GeaJsxElement, TouchEventHandler } from '@geastack/core'

export type VirtualListItemTemplate = (index: number, top: number) => any
/** Renders row `index`, placed at `top`. Return an element; wrap plain text in one, such as a `<span>`. */
export type VirtualListItemTemplate = (index: number, top: number) => GeaJsxElement

export interface VirtualListProps {
itemCount: number
Expand Down
4 changes: 2 additions & 2 deletions packages/elements/components/VirtualList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ClassValue, Event, TouchEventHandler } from '@geastack/core'
import type { ClassValue, Event, GeaJsxElement, TouchEventHandler } from '@geastack/core'

// A windowing list backed by the native <virtual-list> element. The element is
// an ordinary overflow:scroll container that scrolls over a virtual content
Expand All @@ -10,7 +10,7 @@ import type { ClassValue, Event, TouchEventHandler } from '@geastack/core'
// blit instead of re-rendering. Row content, height, colours and fonts all come
// from `itemTemplate` + its CSS; the framework owns none of it.

export type VirtualListItemTemplate = (index: number, top: number) => any
export type VirtualListItemTemplate = (index: number, top: number) => GeaJsxElement

export interface VirtualListProps {
itemCount: number
Expand Down
1 change: 1 addition & 0 deletions packages/elements/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { CameraView } from './CameraView'
export { default as VirtualList } from './VirtualList'
export type { VirtualListProps, VirtualListItemTemplate } from './VirtualList'
Loading