Skip to content
Merged
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
2 changes: 2 additions & 0 deletions packages/core/events.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ const char *PointerEvent::typeName() const
return "keydown";
case PointerEventType::Rotary:
return "rotary";
case PointerEventType::Scroll:
return "scroll";
}
return "";
}
Expand Down
3 changes: 2 additions & 1 deletion packages/core/include/events.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ enum class PointerEventType {
Click,
Input,
KeyDown,
Rotary
Rotary,
Scroll
};

enum class EventPhase {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ export interface GeaEventMap {
input: InputEvent
keydown: KeyEvent
rotary: RotaryEvent
scroll: Event
}

export type TouchEventHandler = (event: TouchEvent) => void
Expand Down Expand Up @@ -1523,6 +1524,7 @@ export interface NativeButtonProps extends NativeEventAttributes {
}

export interface NativeVirtualListProps extends NativeEventAttributes {
onScroll?: (event: Event) => void
id?: string
class?: ClassValue
style?: Style
Expand Down
24 changes: 24 additions & 0 deletions packages/core/test/test_gea_engine_rare_data_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,30 @@ int probe()
kTestName, stopChildHits, stopParentHits);
return 33;
}
int nextChildHits = 0;
tree.setEventListener(stopChildId, "click", [&](gea::framework::events::PointerEvent &) { nextChildHits++; });
{
gea::framework::events::PointerEvent ev{};
ev.type = gea::framework::events::PointerEventType::Click;
ev.targetId = stopChildId;
tree.dispatchEvent(ev);
}
if (stopChildHits != 2 || nextChildHits != 1 || stopParentHits != 0) {
std::fprintf(stderr, "[%s] stopPropagation skipped a target listener or reached parent\n", kTestName);
return 39;
}
int parentScrollHits = 0;
tree.setEventListener(perParentId, "scroll", [&](gea::framework::events::PointerEvent &) { parentScrollHits++; });
{
gea::framework::events::PointerEvent ev{};
ev.type = gea::framework::events::PointerEventType::Scroll;
ev.targetId = perChildId;
ev.bubbles = false;
if (tree.dispatchEvent(ev) || parentScrollHits != 0) {
std::fprintf(stderr, "[%s] non-bubbling scroll reached parent without target listener\n", kTestName);
return 40;
}
}

// Removing a node must reclaim its rare-data block (no leak, no stale fire).
tree.removeNode(perChildId);
Expand Down
3 changes: 2 additions & 1 deletion packages/elements/components/VirtualList.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ClassValue, TouchEventHandler } from '@geastack/core'
import type { ClassValue, Event, TouchEventHandler } from '@geastack/core'

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

Expand All @@ -15,6 +15,7 @@ export interface VirtualListProps {
onTouchStart?: TouchEventHandler
onTouchMove?: TouchEventHandler
onTouchEnd?: TouchEventHandler
onScroll?: (event: Event) => void
}

export default function VirtualList(props: VirtualListProps): any
7 changes: 5 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, TouchEventHandler } from '@geastack/core'
import type { ClassValue, Event, 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 @@ -25,6 +25,7 @@ export interface VirtualListProps {
onTouchStart?: TouchEventHandler
onTouchMove?: TouchEventHandler
onTouchEnd?: TouchEventHandler
onScroll?: (event: Event) => void
}

const SLOT_COUNT = 16
Expand Down Expand Up @@ -94,7 +95,8 @@ export default function VirtualList({
itemTemplate,
onTouchStart,
onTouchMove,
onTouchEnd
onTouchEnd,
onScroll
}: VirtualListProps) {
const rowOverscan = overscan === undefined ? 2 : overscan
return (
Expand All @@ -105,6 +107,7 @@ export default function VirtualList({
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
onScroll={onScroll}
>
<VirtualListSlot slotIndex={0} scrollTop={scrollTop} itemHeight={itemHeight} overscan={rowOverscan} itemCount={itemCount} itemClass={itemClass} itemTemplate={itemTemplate} />
<VirtualListSlot slotIndex={1} scrollTop={scrollTop} itemHeight={itemHeight} overscan={rowOverscan} itemCount={itemCount} itemClass={itemClass} itemTemplate={itemTemplate} />
Expand Down
19 changes: 18 additions & 1 deletion packages/elements/ui/virtual_list.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ namespace gea::embedded::ui {

namespace {

void dispatchScrollEvent(int node)
{
Tree &tree = Tree::instance();
if (!tree.hasListenersForType("scroll")) return;
gea::framework::events::PointerEvent event{};
event.type = gea::framework::events::PointerEventType::Scroll;
event.targetId = node;
event.bubbles = false;
event.cancelable = false;
tree.dispatchEvent(event);
}

int parseIntAttribute(const char *value, int fallback)
{
if (!value || !value[0]) return fallback;
Expand Down Expand Up @@ -225,6 +237,7 @@ bool VirtualListRenderer::setScrollTop(int node, int scrollTop)
n.render.dirty = 1;
n.render.layout_dirty = 1;
Tree::instance().markScrollDirty(node);
dispatchScrollEvent(node);
return true;
}

Expand Down Expand Up @@ -268,7 +281,11 @@ void Tree::setScrollTop(int node, int scrollTop)
{
if (node < 0 || node >= nodeCount()) return;
Node &n = treeState().nodes[node];
if (!isViewLikeNodeType(n.type) || (n.type != NodeType::VirtualList && !scrollsOverflowY(n.style))) return;
if (n.type == NodeType::VirtualList) {
VirtualListRenderer::setScrollTop(node, scrollTop);
return;
}
if (!isViewLikeNodeType(n.type) || !scrollsOverflowY(n.style)) return;
const int maxY = ViewRenderer::scrollMaxY(n);
if (scrollTop < 0) scrollTop = 0;
if (scrollTop > maxY) scrollTop = maxY;
Expand Down
9 changes: 6 additions & 3 deletions packages/engine/ui/tree_events.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ void applyAudioAttribute(Tree &tree, int node, const char *name, const char *val
// Index order mirrors NodeEventListeners' slots and is kept in sync by
// setEventListener (on the empty->set transition) and NodeEventListeners::clear()
// (which every removal path calls).
constexpr int kEventTypeCount = 6;
constexpr int kEventTypeCount = 7;
int g_listenerTypeCounts[kEventTypeCount] = {};
EventListenerId g_nextEventListenerId = 1;

Expand All @@ -75,6 +75,7 @@ int eventTypeIndex(const char *type)
if (sameName(type, "touchend") || sameName(type, "pointerup")) return 3;
if (sameName(type, "input")) return 4;
if (sameName(type, "keydown")) return 5;
if (sameName(type, "scroll")) return 6;
return -1;
}

Expand Down Expand Up @@ -276,6 +277,7 @@ void NodeEventListeners::clear()
release(touchend, 3);
release(input, 4);
release(keydown, 5);
release(scroll, 6);
}

NodeEventListenerList *NodeEventListeners::listenersFor(const char *type)
Expand All @@ -287,6 +289,7 @@ NodeEventListenerList *NodeEventListeners::listenersFor(const char *type)
if (sameName(type, "touchend") || sameName(type, "pointerup")) return &touchend;
if (sameName(type, "input")) return &input;
if (sameName(type, "keydown")) return &keydown;
if (sameName(type, "scroll")) return &scroll;
return nullptr;
}

Expand All @@ -298,7 +301,7 @@ const NodeEventListenerList *NodeEventListeners::listenersFor(const char *type)
bool NodeEventListeners::hasAny() const
{
return !click.empty() || !touchstart.empty() || !touchmove.empty() ||
!touchend.empty() || !input.empty() || !keydown.empty();
!touchend.empty() || !input.empty() || !keydown.empty() || !scroll.empty();
}

EventListenerId Tree::setEventListener(int node, const char *type, gea::framework::events::EventListener listener)
Expand Down Expand Up @@ -367,6 +370,7 @@ bool Tree::dispatchEvent(gea::framework::events::PointerEvent &event)
event.target = gea::framework::events::EventTarget(event.targetId);
bool dispatched = false;
for (int node = event.targetId; node >= 0 && node < state.nodeCount; node = state.nodes[node].parent) {
if (!event.bubbles && node != event.targetId) break;
NodeRareData *rd = rareDataFor(node);
if (!rd) continue;
auto *initialList = rd->listeners.listenersFor(event.typeName());
Expand Down Expand Up @@ -408,7 +412,6 @@ bool Tree::dispatchEvent(gea::framework::events::PointerEvent &event)
cursor = nextId;
(*callback)(event);
dispatched = true;
if (event.propagationStopped || !event.bubbles) break;
}
if (event.propagationStopped || !event.bubbles) break;
}
Expand Down
1 change: 1 addition & 0 deletions packages/engine/ui/tree_events.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ struct NodeEventListeners {
NodeEventListenerList touchend;
NodeEventListenerList input;
NodeEventListenerList keydown;
NodeEventListenerList scroll;

void clear();
NodeEventListenerList *listenersFor(const char *type);
Expand Down
17 changes: 17 additions & 0 deletions packages/geatsc-plugin-gea/src/host-shims.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ const NODE_HANDLE_RECEIVER_TYPES = [
// `NODE_HANDLE_RECEIVER_TYPES` above states.
const EVENT_RECEIVER_TYPES = [
"gea::framework::events::PointerEvent",
// The base interface every event extends, and the parameter `onScroll` and
// `EventTarget.addEventListener(type, listener)` declare.
"Event",
"PointerEvent",
"TouchEvent",
"RotaryEvent",
Expand Down Expand Up @@ -318,6 +321,11 @@ export function createGeaHostShims(): HostShimDefinitions {
PointerEvent: "gea::framework::events::PointerEvent",
TouchEvent: "gea::framework::events::PointerEvent",
RotaryEvent: "gea::framework::events::PointerEvent",
// The base interface, for `onScroll={e => ...}` and any listener typed by
// `EventTarget.addEventListener`. Without it `e` lowered to a synthesized
// record the listener adapter cannot build from `PointerEvent&`, so the
// app aborted on the first scroll.
Event: "gea::framework::events::PointerEvent",
TouchPoint: "gea::framework::events::TouchPoint",
// The text-input half of the same join. `onInput={e => ...}` /
// `onKeyDown={e => ...}` declare `InputEvent` / `KeyEvent` (index.d.ts),
Expand Down Expand Up @@ -1613,6 +1621,15 @@ export function createGeaHostShims(): HostShimDefinitions {
// member spellings, so emission refused `TouchEvent.clientX` by name --
// the fail-closed guard reporting, correctly, that this table was the
// thing missing.
// `Event.type`: the engine keeps the kind as an enum and names it with
// `typeName()` (events.cpp), the same string a listener was bound for.
type: [
{
emit: "std::string(({receiver}).typeName())",
returnType: "std::string",
receiverTypes: EVENT_RECEIVER_TYPES,
},
],
pointerId: [
{
emit: "static_cast<double>(({receiver}).pointerId)",
Expand Down
20 changes: 20 additions & 0 deletions packages/geatsc-plugin-gea/test/host-shims-protocol.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,26 @@ import test from 'node:test'

import { createGeaHostShims } from '../dist/host-shims.js'

// A listener parameter declared as any event interface index.d.ts publishes is
// handed the engine's one event struct. `Event` is declared by `onScroll` and by
// `EventTarget.addEventListener(type, listener: (event: Event) => void)`; without
// a carrier it lowered to a synthesized record the runtime listener adapter
// cannot build from a PointerEvent, and the app aborted on the first scroll.
test('every declared event parameter type lowers to the engine event, including Event', () => {
const definitions = createGeaHostShims()
for (const name of ['Event', 'PointerEvent', 'TouchEvent', 'RotaryEvent', 'InputEvent', 'KeyEvent']) {
assert.equal(definitions.nativeTypes?.[name], 'gea::framework::events::PointerEvent', `${name} must lower to the engine event`)
}
for (const member of ['type', 'target', 'currentTarget']) {
const rows = definitions.nativeMemberPropertyGetters?.[member] ?? []
assert.ok(rows.some(row => row.receiverTypes?.includes('Event')), `Event.${member} must be a native member read`)
}
for (const method of ['preventDefault', 'stopPropagation']) {
const rows = definitions.nativeMemberMethods?.[method] ?? []
assert.ok(rows.some(row => row.receiverTypes?.includes('Event')), `Event.${method}() must be a native member call`)
}
})

test('geolocation snapshot operations publish their non-throwing physical contract', () => {
const definitions = createGeaHostShims()
for (const namespace of ['Geolocation', 'geolocation']) {
Expand Down
Loading