Pull-to-refresh (external piece)
Authoring
use day_piece_pullrefresh::pull_to_refresh;
let refreshing = Signal::new(false);
// One reload path for EVERY begin — pull gesture, dayscript toggle, or a button:
watch(move || refreshing.get(), move |now, _| {
if *now {
let done = refreshing.setter();
std::thread::spawn(move || {
reload(); // off the UI thread
done.set(false); // completion hops back and dismisses the indicator
});
}
});
pull_to_refresh(refreshing, scroll(rows)).id("feed-refresh") // or list(items, …)
The bound refreshing: Signal<bool> is two-way (the same contract as UIRefreshControl,
SwipeRefreshLayout.setRefreshing, and ArkUI Refresh):
- a user pull sets it
true(and runs the optional.on_refresh(f)sugar); - the app sets it
falsewhen the reload completes (from a thread viaSignal::setter, or insideday::task); - the app may set it
trueto begin a refresh programmatically (a toolbar button, ⌘R, …).
Prefer the watch-on-the-signal idiom above over .on_refresh when a programmatic path exists;
it makes every begin take the same route. day_piece_pullrefresh::support() reports the compiled
backend’s tier (Native / Emulated).
Scripting
The piece’s node accepts Event::ToggleChanged as a synthetic begin/end, so the existing dayscript
toggle: step drives it identically on every backend:
- toggle: { id: feed-refresh, value: true } # begin (runs the app's reload)
- assert_text: { id: refresh-status, key: refresh_status_refreshing }
Per-toolkit realization
| Target | Tier | Mechanism |
|---|---|---|
| ios-uikit | Native | A passthrough host view attaches a UIRefreshControl to the descendant UIScrollView when day mounts it (didAddSubview:, which covers list(), since a UITableView is a UIScrollView). |
| android-mdc | Native | This crate’s DayPullRefresh extends SwipeRefreshLayout (AndroidX, added to Gradle via [package.metadata.day.android]); the scrollable mounts directly into it. |
| harmony-arkui | Native | ARKUI_NODE_REFRESH created by this crate’s own NDK shim; pull events via NODE_REFRESH_ON_REFRESH, indicator via NODE_REFRESH_REFRESHING. |
| macos-appkit | Emulated | Spinner-chip overlay + the pull gesture from elastic scrolling: the clip view’s bounds go negative during a trackpad rubber-band; crossing ~60 pt begins a refresh. |
| gtk | Emulated | Overlay + GtkScrolledWindow::edge-overshot (Top), GTK’s purpose-built overshoot signal. |
| qt / xaml | Emulated | Overlay + programmatic only (desktop Qt has no elastic overscroll; XAML’s RefreshContainer is touch-only; a native tier is a follow-up for touch devices). |
| mock | Emulated | Composition path; drives the piece’s tests via ToggleChanged. |
The emulated indicator is the built-in spinner() in a floating chip, shown while refreshing:
pure composition (when + overlay container) with no per-backend code.
The container-piece recipe
On the wrap-based platforms the realized node is the native refresh wrapper and day mounts the
scrollable as a Day child inside it; see docs/extending.md (“Container pieces”) for the
cx.native + fill-layout + cx.under recipe this piece establishes.
Limits
- The wrapped child should be the scrollable (
scroll(...)orlist(...)). If the child’s realized view isn’t scroll-backed, the pull gesture is inert (the overlay + programmatic path still work). - iOS: a programmatic begin shows the control’s spinner without auto-revealing it (UIKit’s standard behavior); a user pull reveals it naturally.
- Emulated gesture thresholds (AppKit ~60 pt, GTK overshoot) are heuristics tuned in the piece.