From 21241155c5c7a793f66dd3b71313209ca5142b1a Mon Sep 17 00:00:00 2001 From: Ozgur Ersoy Date: Sat, 17 Oct 2026 20:42:59 +0200 Subject: [PATCH 1/2] Elements: Type VirtualList's item template as an element VirtualListItemTemplate returned any. Every slot renders {itemTemplate(...)}, and an any result lowers to gea::Value, which gea::jsx::child has no rule for, so an app that used the component stopped at a static_assert in the C++ build. No app or test imported the component, so nothing caught it (#70). The template's result is now GeaJsxElement, the type core uses for a component's returned element. It lowers to an element handle that gea::jsx::child appends. The .d.ts says to wrap plain text in an element: TypeScript still accepts a string because GeaJsxElement is an empty interface, but geatsc then refuses it at compile time with a named conversion error. Tests: added run-gea-elements-virtual-list-pipeline.sh, which builds the fixture packages/core/test/fixtures/gea-elements-virtual-list (the component used the way the README shows) and checks that the list takes its height from its class and that its 16 slots show rows 0..15 once each. It passes. With the template typed any again the C++ build fails with the static_assert, and with vlIndexForSlot shifted by one the row check fails. The fixture type-checks with tsc 5.9.3 (strict). Scrolling is not tested: onScroll waits on geastack/compiler#4, and a scrollTop change replaces the node (#70). --- .../gea-elements-virtual-list/App.tsx | 12 +++ .../gea-elements-virtual-list/Rows.tsx | 27 ++++++ .../gea-elements-virtual-list/index.tsx | 5 + .../gea-elements-virtual-list/styles.css | 13 +++ .../run-gea-elements-virtual-list-pipeline.sh | 9 ++ .../test_gea_elements_virtual_list_main.cpp | 95 +++++++++++++++++++ packages/elements/components/VirtualList.d.ts | 5 +- packages/elements/components/VirtualList.tsx | 4 +- 8 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 packages/core/test/fixtures/gea-elements-virtual-list/App.tsx create mode 100644 packages/core/test/fixtures/gea-elements-virtual-list/Rows.tsx create mode 100644 packages/core/test/fixtures/gea-elements-virtual-list/index.tsx create mode 100644 packages/core/test/fixtures/gea-elements-virtual-list/styles.css create mode 100755 packages/core/test/run-gea-elements-virtual-list-pipeline.sh create mode 100644 packages/core/test/test_gea_elements_virtual_list_main.cpp diff --git a/packages/core/test/fixtures/gea-elements-virtual-list/App.tsx b/packages/core/test/fixtures/gea-elements-virtual-list/App.tsx new file mode 100644 index 0000000..8e16996 --- /dev/null +++ b/packages/core/test/fixtures/gea-elements-virtual-list/App.tsx @@ -0,0 +1,12 @@ +import { Component } from '@geastack/core' +import { Rows } from './Rows' + +export class App extends Component { + template() { + return ( +
+ +
+ ) + } +} diff --git a/packages/core/test/fixtures/gea-elements-virtual-list/Rows.tsx b/packages/core/test/fixtures/gea-elements-virtual-list/Rows.tsx new file mode 100644 index 0000000..fc7a794 --- /dev/null +++ b/packages/core/test/fixtures/gea-elements-virtual-list/Rows.tsx @@ -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 ( + {'Row ' + index}} + onScroll={() => { + const list = document.getElementById('rows') as VirtualListElement | null + if (list) rows.scrollTop = list.scrollTop + }} + /> + ) +} diff --git a/packages/core/test/fixtures/gea-elements-virtual-list/index.tsx b/packages/core/test/fixtures/gea-elements-virtual-list/index.tsx new file mode 100644 index 0000000..b83d2c3 --- /dev/null +++ b/packages/core/test/fixtures/gea-elements-virtual-list/index.tsx @@ -0,0 +1,5 @@ +import { mount } from '@geastack/core' +import { App } from './App' +import './styles.css' + +mount(App) diff --git a/packages/core/test/fixtures/gea-elements-virtual-list/styles.css b/packages/core/test/fixtures/gea-elements-virtual-list/styles.css new file mode 100644 index 0000000..b1d09d7 --- /dev/null +++ b/packages/core/test/fixtures/gea-elements-virtual-list/styles.css @@ -0,0 +1,13 @@ +.app { + width: 100vw; + height: 100vh; +} + +.rows { + width: 100vw; + height: 400px; +} + +.row-label { + font-size: 16px; +} diff --git a/packages/core/test/run-gea-elements-virtual-list-pipeline.sh b/packages/core/test/run-gea-elements-virtual-list-pipeline.sh new file mode 100755 index 0000000..06522da --- /dev/null +++ b/packages/core/test/run-gea-elements-virtual-list-pipeline.sh @@ -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" diff --git a/packages/core/test/test_gea_elements_virtual_list_main.cpp b/packages/core/test/test_gea_elements_virtual_list_main.cpp new file mode 100644 index 0000000..bca7244 --- /dev/null +++ b/packages/core/test/test_gea_elements_virtual_list_main.cpp @@ -0,0 +1,95 @@ +#include "native_test_harness.h" + +#include "app.h" +#include "ui/tree_internal.h" + +#include +#include +#include +#include + +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 visibleRowIndexes() +{ + std::vector 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 seen(kSlotCount, false); + bool ok = indexes.size() == static_cast(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(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 node, which resets the native offset. + + if (!ok) { + dumpTree(kTest); + return 1; + } + return 0; +} diff --git a/packages/elements/components/VirtualList.d.ts b/packages/elements/components/VirtualList.d.ts index 814c041..558436e 100644 --- a/packages/elements/components/VirtualList.d.ts +++ b/packages/elements/components/VirtualList.d.ts @@ -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 ``. */ +export type VirtualListItemTemplate = (index: number, top: number) => GeaJsxElement export interface VirtualListProps { itemCount: number diff --git a/packages/elements/components/VirtualList.tsx b/packages/elements/components/VirtualList.tsx index 1ca393d..d701dda 100644 --- a/packages/elements/components/VirtualList.tsx +++ b/packages/elements/components/VirtualList.tsx @@ -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 element. The element is // an ordinary overflow:scroll container that scrolls over a virtual content @@ -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 From a6771b73db71a3306ec67f3472357c86a7acde63 Mon Sep 17 00:00:00 2001 From: Ozgur Ersoy Date: Sun, 18 Oct 2026 15:02:10 +0200 Subject: [PATCH 2/2] Elements: Export the VirtualList types from the components index A native build resolves value imports of @geastack/elements to components/index.ts, not index.d.ts. index.ts exported VirtualList but not VirtualListProps or VirtualListItemTemplate, so an app that wrote `import { VirtualList, type VirtualListItemTemplate } from '@geastack/elements'` type-checked but failed to compile natively with "has no exported member 'VirtualListItemTemplate'". index.ts now re-exports both types as index.d.ts does. CameraViewProps has the same gap and is left for a separate change. Tests: a scratch app importing VirtualList, VirtualListProps and VirtualListItemTemplate this way, built with run-gea-native-app-pipeline.sh: fails with that error before the change, builds and renders its rows after it. run-gea-elements-virtual-list-pipeline.sh still passes. --- packages/elements/components/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/elements/components/index.ts b/packages/elements/components/index.ts index 992a63c..705bad0 100644 --- a/packages/elements/components/index.ts +++ b/packages/elements/components/index.ts @@ -1,2 +1,3 @@ export { CameraView } from './CameraView' export { default as VirtualList } from './VirtualList' +export type { VirtualListProps, VirtualListItemTemplate } from './VirtualList'