Skip to content

Scroll list with clickable items on mobile #18923

Open
@Maksych

Description

@Maksych

What problem does this solve or what need does it fill?

Scroll list with clickable items on mobile. Need a way to know when the pointer is dragged when the pointer is released/click.

What solution would you like?

//! Scrollable list with clickable items in 0.16.0 on mobile.
//!
//! Scroll by drag and click only without drag started.
//!
//! Maybe something like `PointerDragging` and `update_pointer_dragging` must be implemented in bevy?
//!
//! I write small example how I do this:

use bevy::{
    color::palettes::tailwind, picking::pointer::PointerId, platform::collections::HashSet,
    prelude::*,
};

fn spawn_camera(mut commands: Commands) {
    commands.spawn(Camera2d);
}

fn spawn_ui(mut commands: Commands) {
    commands
        .spawn(Node {
            width: Val::Percent(100.),
            height: Val::Percent(100.),
            overflow: Overflow::scroll_y(),
            flex_wrap: FlexWrap::Wrap,
            row_gap: Val::Px(10.),
            ..Default::default()
        })
        .observe(scroll_drag)
        .with_children(|s| {
            for i in 1..=100 {
                s.spawn((
                    Node {
                        width: Val::Percent(100.),
                        justify_content: JustifyContent::Center,
                        align_items: AlignItems::Center,
                        padding: UiRect::vertical(Val::Px(20.)),
                        border: UiRect::all(Val::Px(2.)),
                        ..Default::default()
                    },
                    BackgroundColor(tailwind::GRAY_500.into()),
                    BorderColor(tailwind::GRAY_300.into()),
                ))
                .observe(item_click)
                .with_child(Text::new(format!("Item #{i}")));
            }
        });
}

fn scroll_drag(trigger: Trigger<Pointer<Drag>>, mut node_query: Query<&mut ScrollPosition>) {
    let Ok(mut scroll_position) = node_query.get_mut(trigger.target()) else {
        return;
    };
    scroll_position.offset_y -= trigger.delta.y;
}

fn item_click(
    trigger: Trigger<Pointer<Released>>,
    state: Res<PointerDragging>,
    text_query: Query<(&ChildOf, &Text)>,
) {
    if state.contains(&(trigger.pointer_id, trigger.button)) {
        return;
    }
    if let Some(text) = text_query
        .iter()
        .find_map(|(child_of, text)| (trigger.target() == child_of.parent()).then_some(text))
    {
        println!("Click at: {}", text.0);
    };
}

#[derive(Debug, Default, Deref, DerefMut, Resource)]
struct PointerDragging(HashSet<(PointerId, PointerButton)>);

fn update_pointer_dragging(
    mut state: ResMut<PointerDragging>,
    mut drag_start_events: EventReader<Pointer<DragStart>>,
    mut drag_end_events: EventReader<Pointer<DragEnd>>,
) {
    for drag_start_event in drag_start_events.read() {
        state.insert((drag_start_event.pointer_id, drag_start_event.button));
    }

    for drag_end_event in drag_end_events.read() {
        state.remove(&(drag_end_event.pointer_id, drag_end_event.button));
    }
}

fn main() {
    App::new()
        .init_resource::<PointerDragging>()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, (spawn_camera, spawn_ui))
        .add_systems(Update, update_pointer_dragging)
        .run();
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-UIGraphical user interfaces, styles, layouts, and widgetsC-FeatureA new feature, making something new possible

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions