Skip to content

PullToRefresh

Header: winui/ui/scrolling/PullToRefresh.hpp. Spec: winui/ui/scrolling/PullToRefresh.hpp.

A container that refreshes its content when it is pulled past its start, with a progress indicator. The pull is a mouse drag.

A pull starts once a drag mostly along pull_direction() passes 8 DIPs, and only while the content is scrolled to the edge the pull comes from. The content is anything with Element::scroll_info(), such as a Scroll or a ListView. It follows the pointer, and a 100 DIP visualizer slides in behind it, its indicator turning with the pull. The indicator is a Refresh symbol, or what set_visualizer_content() installs. Releasing past 80 % of the visualizer (Pending) refreshes: the state goes to Refreshing, on_refresh_requested() fires once, and the refresh lasts until the app calls complete_refresh(). ui::PullToRefresh is the declarative form.

Example:

ui::Control<ui::PullToRefresh> refresh;
window.set_content(ui::PullToRefresh{
    .height               = 400.0f,
    .on_refresh_requested = [&refresh] {
        // Start reloading; when the new data is in (now or later, e.g. from a timer):
        refresh->complete_refresh(); // back to Idle
    },
    .control              = refresh,
    .content              = ui::Scroll{.content = ui::VerticalStack{.content = {
        ui::Text{.text = "Item 1"},
        ui::Text{.text = "Item 2"},
    }}},
});

// Imperatively, pulling up from the bottom:
auto ptr = std::make_unique<ui::PullToRefreshControl>();
ui::ScrollControl* scroll = ptr->set_content(std::make_unique<ui::ScrollControl>());
ptr->set_pull_direction(ui::RefreshPullDirection::BottomToTop);
ptr->on_refresh_state_changed([](const ui::PullToRefreshStateChangedArgs& args) { /* e.g. show a status text */ });
ptr->request_refresh(); // refresh programmatically, without a pull

ui::PullToRefresh

Pull one child with the mouse to refresh it. Builds a ui::PullToRefreshControl. on_refresh_requested fires once, and the app calls .control's complete_refresh() when the work it started, typically an async data fetch, has finished.

Example:

void start_fetch(ui::Action done); // the app's own async work
ui::Control<ui::PullToRefresh> pull;

window.set_content(ui::PullToRefresh{
    .height               = 400.0f,
    .on_refresh_requested = [&]
    {
        start_fetch([&] { pull->complete_refresh(); });
    },
    .control = pull,
    .content = ui::Scroll{.content = ui::VerticalStack{.content = {
        ui::Text{.text = "Item 1"},
        ui::Text{.text = "Item 2"},
    }}},
});

Every field, in the order a designated initializer names them:

ui::PullToRefresh{
    .pull_direction, .visualizer_orientation, .style, .margin, .width, .height, .min_width,
    .min_height, .max_width, .max_height, .horizontal_alignment, .vertical_alignment, .visible,
    .tool_tip, .accessibility, .on_refresh_requested, .on_refresh_state_changed,
    .on_pointer_entered, .on_pointer_exited, .on_got_focus, .on_lost_focus, .on_key_down,
    .on_wheel, .control, .content, .visualizer_content
}

Fields, in declaration order (designators must follow it):

Field Type Default
pull_direction ui::Binding<ui::RefreshPullDirection> {ui::RefreshPullDirection::TopToBottom} Which edge the content is pulled from.
visualizer_orientation ui::Binding<ui::RefreshVisualizerOrientation> {ui::RefreshVisualizerOrientation::Auto} How the indicator is turned. Auto turns it along the pull.
on_refresh_requested ui::Callback<void()> Fires once per refresh; call complete_refresh() on .control when done.
on_refresh_state_changed ui::Callback<void(const ui::PullToRefreshStateChangedArgs&)> Fires with the old and the new state (Idle, Interacting, Pending, Refreshing).
on_pointer_entered ui::Action The pointer moved onto the control.
on_pointer_exited ui::Action The pointer left the control.
on_got_focus ui::Action Keyboard focus arrived on the control, or on anything inside it, from outside it. A control with nothing focusable in it never gets focus, and never raises this.
on_lost_focus ui::Action Keyboard focus left the control and everything inside it.
on_key_down ui::Callback<ui::Handling(const ui::KeyEvent&)> A key pressed while focus is on the control or inside it, before the control's own handling; ui::Handled stops it there (Element::on_key_down()).
on_wheel ui::Callback<ui::Handling(const ui::PointerEvent&)> A wheel turn over the control, before the control scrolls; ui::Handled stops it there (Element::on_wheel()).
control ui::ControlOut<ui::PullToRefresh> Out: the handle that receives the built control.
content ui::Spec The pulled content, typically a ui::Scroll or a list.
visualizer_content ui::Spec What turns as the list is pulled, in place of the Refresh symbol.

Plus the common fields style, margin, width, height, min_width, min_height, max_width, max_height, horizontal_alignment, vertical_alignment, visible, tool_tip, accessibility every spec carries (conventions).

build

std::unique_ptr<Element> build()&&

Consumes the spec and returns the built ui::PullToRefreshControl; the caller owns it.

ui::PullToRefreshControl

Derives from ui::Element.

A container that refreshes its content when it is pulled past its start, with a progress indicator. The pull is a mouse drag.

A pull starts once a drag mostly along pull_direction() passes 8 DIPs, and only while the content is scrolled to the edge the pull comes from. The content is anything with Element::scroll_info(), such as a Scroll or a ListView. It follows the pointer, and a 100 DIP visualizer slides in behind it, its indicator turning with the pull. The indicator is a Refresh symbol, or what set_visualizer_content() installs. Releasing past 80 % of the visualizer (Pending) refreshes: the state goes to Refreshing, on_refresh_requested() fires once, and the refresh lasts until the app calls complete_refresh(). ui::PullToRefresh is the declarative form.

Example:

ui::Control<ui::PullToRefresh> refresh;
window.set_content(ui::PullToRefresh{
    .height               = 400.0f,
    .on_refresh_requested = [&refresh] {
        // Start reloading; when the new data is in (now or later, e.g. from a timer):
        refresh->complete_refresh(); // back to Idle
    },
    .control              = refresh,
    .content              = ui::Scroll{.content = ui::VerticalStack{.content = {
        ui::Text{.text = "Item 1"},
        ui::Text{.text = "Item 2"},
    }}},
});

// Imperatively, pulling up from the bottom:
auto ptr = std::make_unique<ui::PullToRefreshControl>();
ui::ScrollControl* scroll = ptr->set_content(std::make_unique<ui::ScrollControl>());
ptr->set_pull_direction(ui::RefreshPullDirection::BottomToTop);
ptr->on_refresh_state_changed([](const ui::PullToRefreshStateChangedArgs& args) { /* e.g. show a status text */ });
ptr->request_refresh(); // refresh programmatically, without a pull

PullToRefreshControl

PullToRefreshControl()

Pulls TopToBottom, with the default Refresh-symbol indicator and no content.

~PullToRefreshControl

~PullToRefreshControl() override

set_content

template <typename T>
T* set_content(std::unique_ptr<T> content)

Sets the content that is pulled and takes ownership of it. Returns a non-owning pointer to it.

content

Element* content() const

The pulled content (non-owning), or nullptr.

set_visualizer_content

template <typename T>
T* set_visualizer_content(std::unique_ptr<T> content)

What spins as the list is pulled, instead of the Refresh symbol.

visualizer_content

Element* visualizer_content() const

The indicator element (non-owning): the default Refresh symbol, or what set_visualizer_content() installed.

pull_direction

RefreshPullDirection pull_direction() const

Which edge the content is pulled from; TopToBottom by default. Changing it mid-pull ends the pull.

set_pull_direction

void set_pull_direction(RefreshPullDirection direction)

Lays the control out again and redraws.

visualizer_orientation

RefreshVisualizerOrientation visualizer_orientation() const

How the indicator is turned; Auto by default.

set_visualizer_orientation

void set_visualizer_orientation(RefreshVisualizerOrientation orientation)

Marks the control for redraw.

state

PullToRefreshState state() const

The visualizer's current state; Idle by default.

pull_distance

float pull_distance() const

How far the content is pushed along the pull, in [0, 100].

interaction_ratio

float interaction_ratio() const

How far the pull has come, as pull_distance() over the visualizer's 100 DIPs, in [0, 1].

request_refresh

void request_refresh()

Starts a refresh at once and raises on_refresh_requested. Does nothing while refreshing.

complete_refresh

void complete_refresh()

Ends the refresh and goes back to Idle.

on_refresh_state_changed

void on_refresh_state_changed(Callback<void(const PullToRefreshStateChangedArgs&)> handler)

Fires with the old and the new state.

on_refresh_requested

void on_refresh_requested(Callback<void()> handler)

Fires when a refresh starts; the refresh lasts until complete_refresh().

for_each_child

void for_each_child(ChildVisitor visit) const override

Visits the content only. The indicator takes no focus, so it is added separately to the visual tree and the accessibility tree while it shows (below).

automation_children

void automation_children(std::vector<Element*>& out) const override

The content, then the indicator while it is pulled into view.

build_visual_tree_override

void build_visual_tree_override(VisualTree& tree) const override

Adds the indicator while it is pulled into view.

measure_override

Size measure_override(Size available, const MeasureContext& context) const override

Implement these instead of measure() and arrange(). The base class has already applied margin, size limits and alignment by the time these run. measure_override() is pure (every concrete Element must size itself); arrange_override() defaults to a no-op since most leaves need nothing beyond having bounds() set, which the base already did.

arrange_override

void arrange_override(const Rect& final_rect, const ArrangeContext& context) override

Position children within the rect arrange() settled on (also in bounds(), window DIPs). Default: nothing, for a leaf.

render_override

void render_override(Renderer& renderer, const RenderContext& context) const override

What the control draws, in layout coordinates; render() has already applied the visual layer. A container draws its children through their render(), never their render_override().

pointer_override

bool pointer_override(const PointerEvent& e) override

Drives the pull from a mouse drag; other pointer input goes to the content.

handled_pointer_override

void handled_pointer_override(const PointerEvent& e) override

A press and release the content took still start and end a pull.

clear_dirty_override

void clear_dirty_override(DirtyFlags flags) override

Clears the dirty flags of the content and of the indicator, whether it shows or not.

tick_override

void tick_override(Duration delta) override

Advances the settle, Pending pulse and Refreshing spin animations, and ticks the content and the indicator.

struct PullToRefreshStateChangedArgs

What on_refresh_state_changed gets: the state before and after the change.

Field Type Default
old_state PullToRefreshState PullToRefreshState::Idle The state it left.
new_state PullToRefreshState PullToRefreshState::Idle The state it is in now.

Enumerations

RefreshPullDirection

enum class RefreshPullDirection

Which way the content is pulled.

Value
LeftToRight Pulled rightwards from the left edge, while scrolled to the start.
TopToBottom Pulled down from the top, while scrolled to the start (the default).
RightToLeft Pulled leftwards from the right edge, while scrolled to the end.
BottomToTop Pulled up from the bottom, while scrolled to the end.

RefreshVisualizerOrientation

enum class RefreshVisualizerOrientation

How the indicator is turned.

Value
Auto Turned along the pull.
Normal Upright.
Rotate90DegreesCounterclockwise Turned by +90 degrees.
Rotate270DegreesCounterclockwise Turned by -90 degrees.

PullToRefreshState

enum class PullToRefreshState

Where a pull to refresh is: idle, being pulled, armed or refreshing.

Value
Idle Not pulled and not refreshing.
Interacting Pulled, but not far enough to refresh.
Pending Pulled far enough, so a release now refreshes.
Refreshing Refreshing until complete_refresh().

Tests

  • tests/test_pull_to_refresh.cpp