Path: blob/main/examples/ui/scroll_and_overflow/scroll.rs
9331 views
//! This example illustrates scrolling in Bevy UI.12use accesskit::{Node as Accessible, Role};3use bevy::{4a11y::AccessibilityNode,5color::palettes::css::{BLACK, BLUE, RED},6ecs::spawn::SpawnIter,7input::mouse::{MouseScrollUnit, MouseWheel},8picking::hover::HoverMap,9prelude::*,10};1112fn main() {13let mut app = App::new();1415app.add_plugins(DefaultPlugins)16.add_systems(Startup, setup)17.add_systems(Update, send_scroll_events)18.add_observer(on_scroll_handler);1920app.run();21}2223const LINE_HEIGHT: f32 = 21.;2425/// Injects scroll events into the UI hierarchy.26fn send_scroll_events(27mut mouse_wheel_reader: MessageReader<MouseWheel>,28hover_map: Res<HoverMap>,29keyboard_input: Res<ButtonInput<KeyCode>>,30mut commands: Commands,31) {32for mouse_wheel in mouse_wheel_reader.read() {33let mut delta = -Vec2::new(mouse_wheel.x, mouse_wheel.y);3435if mouse_wheel.unit == MouseScrollUnit::Line {36delta *= LINE_HEIGHT;37}3839if keyboard_input.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]) {40std::mem::swap(&mut delta.x, &mut delta.y);41}4243for pointer_map in hover_map.values() {44for entity in pointer_map.keys().copied() {45commands.trigger(Scroll { entity, delta });46}47}48}49}5051/// UI scrolling event.52#[derive(EntityEvent, Debug)]53#[entity_event(propagate, auto_propagate)]54struct Scroll {55entity: Entity,56/// Scroll delta in logical coordinates.57delta: Vec2,58}5960fn on_scroll_handler(61mut scroll: On<Scroll>,62mut query: Query<(&mut ScrollPosition, &Node, &ComputedNode)>,63) {64let Ok((mut scroll_position, node, computed)) = query.get_mut(scroll.entity) else {65return;66};6768let max_offset = (computed.content_size() - computed.size()) * computed.inverse_scale_factor();6970let delta = &mut scroll.delta;71if node.overflow.x == OverflowAxis::Scroll && delta.x != 0. {72// Is this node already scrolled all the way in the direction of the scroll?73let max = if delta.x > 0. {74scroll_position.x >= max_offset.x75} else {76scroll_position.x <= 0.77};7879if !max {80scroll_position.x += delta.x;81// Consume the X portion of the scroll delta.82delta.x = 0.;83}84}8586if node.overflow.y == OverflowAxis::Scroll && delta.y != 0. {87// Is this node already scrolled all the way in the direction of the scroll?88let max = if delta.y > 0. {89scroll_position.y >= max_offset.y90} else {91scroll_position.y <= 0.92};9394if !max {95scroll_position.y += delta.y;96// Consume the Y portion of the scroll delta.97delta.y = 0.;98}99}100101// Stop propagating when the delta is fully consumed.102if *delta == Vec2::ZERO {103scroll.propagate(false);104}105}106107const FONT_SIZE: FontSize = FontSize::Px(20.);108109fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {110// Camera111commands.spawn((Camera2d, IsDefaultUiCamera));112113// Font114let font_handle = asset_server.load("fonts/FiraSans-Bold.ttf");115116// root node117commands118.spawn(Node {119width: percent(100),120height: percent(100),121justify_content: JustifyContent::SpaceBetween,122flex_direction: FlexDirection::Column,123..default()124})125.with_children(|parent| {126// horizontal scroll example127parent128.spawn(Node {129width: percent(100),130flex_direction: FlexDirection::Column,131..default()132})133.with_children(|parent| {134// header135parent.spawn((136Text::new("Horizontally Scrolling list (Ctrl + MouseWheel)"),137TextFont {138font: font_handle.clone().into(),139font_size: FONT_SIZE,140..default()141},142Label,143));144145// horizontal scroll container146parent147.spawn((148Node {149width: percent(80),150margin: UiRect::all(px(10)),151flex_direction: FlexDirection::Row,152overflow: Overflow::scroll_x(), // n.b.153..default()154},155BackgroundColor(Color::srgb(0.10, 0.10, 0.10)),156))157.with_children(|parent| {158for i in 0..100 {159parent160.spawn((161Text(format!("Item {i}")),162TextFont {163font: font_handle.clone().into(),164..default()165},166Label,167AccessibilityNode(Accessible::new(Role::ListItem)),168Node {169min_width: px(200),170align_content: AlignContent::Center,171..default()172},173))174.observe(175|press: On<Pointer<Press>>, mut commands: Commands| {176if press.event().button == PointerButton::Primary {177commands.entity(press.entity).despawn();178}179},180);181}182});183});184185// container for all other examples186parent.spawn((187Node {188width: percent(100),189height: percent(100),190flex_direction: FlexDirection::Row,191justify_content: JustifyContent::SpaceBetween,192..default()193},194children![195vertically_scrolling_list(asset_server.load("fonts/FiraSans-Bold.ttf")),196bidirectional_scrolling_list(asset_server.load("fonts/FiraSans-Bold.ttf")),197bidirectional_scrolling_list_with_sticky(198asset_server.load("fonts/FiraSans-Bold.ttf")199),200nested_scrolling_list(asset_server.load("fonts/FiraSans-Bold.ttf")),201],202));203});204}205206fn vertically_scrolling_list(font_handle: Handle<Font>) -> impl Bundle {207(208Node {209flex_direction: FlexDirection::Column,210justify_content: JustifyContent::Center,211align_items: AlignItems::Center,212width: px(200),213..default()214},215children![216(217// Title218Text::new("Vertically Scrolling List"),219TextFont {220font: font_handle.clone().into(),221font_size: FONT_SIZE,222..default()223},224Label,225),226(227// Scrolling list228Node {229flex_direction: FlexDirection::Column,230align_self: AlignSelf::Stretch,231height: percent(50),232overflow: Overflow::scroll_y(), // n.b.233scrollbar_width: 20.,234..default()235},236#[cfg(feature = "bevy_ui_debug")]237UiDebugOptions {238enabled: true,239outline_border_box: false,240outline_padding_box: false,241outline_content_box: false,242outline_scrollbars: true,243line_width: 2.,244line_color_override: None,245show_hidden: false,246show_clipped: true,247ignore_border_radius: true,248},249BackgroundColor(Color::srgb(0.10, 0.10, 0.10)),250Children::spawn(SpawnIter((0..25).map(move |i| {251(252Node {253min_height: px(LINE_HEIGHT),254max_height: px(LINE_HEIGHT),255..default()256},257children![(258Text(format!("Item {i}")),259TextFont {260font: font_handle.clone().into(),261..default()262},263Label,264AccessibilityNode(Accessible::new(Role::ListItem)),265)],266)267})))268),269],270)271}272273fn bidirectional_scrolling_list(font_handle: Handle<Font>) -> impl Bundle {274(275Node {276flex_direction: FlexDirection::Column,277justify_content: JustifyContent::Center,278align_items: AlignItems::Center,279width: px(200),280..default()281},282children![283(284Text::new("Bidirectionally Scrolling List"),285TextFont {286font: font_handle.clone().into(),287font_size: FONT_SIZE,288..default()289},290Label,291),292(293Node {294flex_direction: FlexDirection::Column,295align_self: AlignSelf::Stretch,296height: percent(50),297overflow: Overflow::scroll(), // n.b.298..default()299},300BackgroundColor(Color::srgb(0.10, 0.10, 0.10)),301Children::spawn(SpawnIter((0..25).map(move |oi| {302(303Node {304flex_direction: FlexDirection::Row,305..default()306},307Children::spawn(SpawnIter((0..10).map({308let value = font_handle.clone();309move |i| {310(311Text(format!("Item {}", (oi * 10) + i)),312TextFont::from(value.clone()),313Label,314AccessibilityNode(Accessible::new(Role::ListItem)),315)316}317}))),318)319})))320)321],322)323}324325fn bidirectional_scrolling_list_with_sticky(font_handle: Handle<Font>) -> impl Bundle {326(327Node {328flex_direction: FlexDirection::Column,329justify_content: JustifyContent::Center,330align_items: AlignItems::Center,331width: px(200),332..default()333},334children![335(336Text::new("Bidirectionally Scrolling List With Sticky Nodes"),337TextFont {338font: font_handle.clone().into(),339font_size: FONT_SIZE,340..default()341},342Label,343),344(345Node {346display: Display::Grid,347align_self: AlignSelf::Stretch,348height: percent(50),349overflow: Overflow::scroll(), // n.b.350grid_template_columns: RepeatedGridTrack::auto(30),351..default()352},353Children::spawn(SpawnIter(354(0..30)355.flat_map(|y| (0..30).map(move |x| (y, x)))356.map(move |(y, x)| {357let value = font_handle.clone();358// Simple sticky nodes at top and left sides of UI node359// can be achieved by combining such effects as360// IgnoreScroll, ZIndex, BackgroundColor for child UI nodes.361let ignore_scroll = BVec2 {362x: x == 0,363y: y == 0,364};365let (z_index, background_color, role) = match (x == 0, y == 0) {366(true, true) => (2, RED, Role::RowHeader),367(true, false) => (1, BLUE, Role::RowHeader),368(false, true) => (1, BLUE, Role::ColumnHeader),369(false, false) => (0, BLACK, Role::Cell),370};371(372Text(format!("|{},{}|", y, x)),373TextFont::from(value.clone()),374TextLayout {375linebreak: LineBreak::NoWrap,376..default()377},378Label,379AccessibilityNode(Accessible::new(role)),380IgnoreScroll(ignore_scroll),381ZIndex(z_index),382BackgroundColor(Color::Srgba(background_color)),383)384})385))386)387],388)389}390391fn nested_scrolling_list(font_handle: Handle<Font>) -> impl Bundle {392(393Node {394flex_direction: FlexDirection::Column,395justify_content: JustifyContent::Center,396align_items: AlignItems::Center,397width: px(200),398..default()399},400children![401(402// Title403Text::new("Nested Scrolling Lists"),404TextFont {405font: font_handle.clone().into(),406font_size: FONT_SIZE,407..default()408},409Label,410),411(412// Outer, bi-directional scrolling container413Node {414column_gap: px(20),415flex_direction: FlexDirection::Row,416align_self: AlignSelf::Stretch,417height: percent(50),418overflow: Overflow::scroll(),419..default()420},421BackgroundColor(Color::srgb(0.10, 0.10, 0.10)),422// Inner, scrolling columns423Children::spawn(SpawnIter((0..5).map(move |oi| {424(425Node {426flex_direction: FlexDirection::Column,427align_self: AlignSelf::Stretch,428height: percent(200. / 5. * (oi as f32 + 1.)),429overflow: Overflow::scroll_y(),430..default()431},432BackgroundColor(Color::srgb(0.05, 0.05, 0.05)),433Children::spawn(SpawnIter((0..20).map({434let value = font_handle.clone();435move |i| {436(437Text(format!("Item {}", (oi * 20) + i)),438TextFont::from(value.clone()),439Label,440AccessibilityNode(Accessible::new(Role::ListItem)),441)442}443}))),444)445})))446)447],448)449}450451452