Compare commits

..

10 Commits
0.1.0 ... main

Author SHA1 Message Date
Piotr Siuszko 17d4cfb263 0.4 2025-05-07 10:38:31 +02:00
Piotr Siuszko 570d00d99a Format fix 2025-01-27 11:15:43 +01:00
Piotr Siuszko bf2f27231a Docs, default scroll view node function, updated sample 2025-01-27 10:56:04 +01:00
Piotr Siuszko 74245f1e20 Update changelog and version 2025-01-24 12:34:35 +01:00
Piotr Siuszko 2c9e8f45ba
Merge pull request #4 from rotcan/topic-scroll-size-fix
Scroll elements size fix
2025-01-24 12:31:26 +01:00
Rahul Srivastava c9f0844c02 Scroll elements size fix 2025-01-15 14:47:37 +05:30
Piotr Siuszko 175914f65c Update README 2024-12-17 13:56:42 +01:00
Piotr Siuszko 6d1fb83636 Last fixes 2024-12-17 13:31:02 +01:00
Piotr Siuszko 2f23d23b02 Plugin ready for Bevy 0.15 2024-11-17 09:06:37 +01:00
Piotr Siuszko 97201b99cc Update to 0.14 2024-07-21 13:35:18 +02:00
7 changed files with 387 additions and 147 deletions

158
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,158 @@
name: Deploy
on:
workflow_dispatch:
inputs:
version:
description: Pass the version
required: true
type: string
release_info:
description: Information about release
required: true
type: string
dry_run:
description: Perform test without releasing
type: choice
required: true
default: "true"
options:
- "true"
- "false"
push:
tags:
- "[0-9]+.[0-9]+.[0-9]+"
permissions:
contents: write
jobs:
setup:
name: Prepare job settings
runs-on: ubuntu-latest
outputs:
version: ${{ steps.setup.outputs.version }}
dry_run: ${{ steps.setup.outputs.dry_run }}
info: ${{ steps.setup.outputs.info }}
steps:
- name: Checkout
uses: actions/checkout@v4
if: ${{ github.event_name == 'push' }}
- name: Get the release version from the tag and info from commit
id: version_push
shell: bash
if: ${{ github.event_name == 'push' }}
run: |
echo version=${GITHUB_REF#refs/tags/} >> $GITHUB_OUTPUT
echo info=$(git tag -l --format='%(contents)' ${GITHUB_REF#refs/tags/}) >> $GITHUB_OUTPUT
- name: Get the release version from the input
id: version_dispatch
shell: bash
if: ${{ github.event_name == 'workflow_dispatch' }}
run: |
echo
echo version=$(echo ${{ inputs.version }} | xargs) >> $GITHUB_OUTPUT
echo dry_run=$(echo ${{ inputs.dry_run }} | xargs) >> $GITHUB_OUTPUT
echo info="${{ inputs.release_info }}" >> $GITHUB_OUTPUT
- name: Setup
id: setup
shell: bash
run: |
echo version=$(if [ -n "${{ steps.version_dispatch.outputs.version }}" ]; then echo "${{ steps.version_dispatch.outputs.version }}"; else echo "${{ steps.version_push.outputs.version }}"; fi) >> $GITHUB_OUTPUT
echo dry_run=$(if [ -n "${{ steps.version_dispatch.outputs.dry_run }}" ]; then echo "${{ steps.version_dispatch.outputs.dry_run }}"; else echo "false"; fi) >> $GITHUB_OUTPUT
echo info=$(if [ -n "${{ steps.version_dispatch.outputs.info }}" ]; then echo "${{ steps.version_dispatch.outputs.info }}"; else echo "${{ steps.version_push.outputs.info }}"; fi) >> $GITHUB_OUTPUT
- name: Display settings
shell: bash
run: echo "Version ${{ steps.setup.outputs.version }}, Dry run- ${{ steps.setup.outputs.dry_run }}, info- ${{ steps.setup.outputs.info }}"
- name: Validate input
shell: bash
run: |
if [ -z "${{ steps.setup.outputs.version }}" ]; then exit 1; fi;
if [ -z "${{ steps.setup.outputs.dry_run }}" ]; then exit 1; fi;
if [ -z "${{ steps.setup.outputs.info }}" ]; then exit 1; fi;
if [[ "${{ steps.setup.outputs.version }}" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+).*?$ ]]; then echo "Valid version"; else echo "INVALID VERSION FORMAT!";exit 1; fi;
build-and-upload:
name: Build and upload
needs:
- setup
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- build: linux
os: ubuntu-latest
target: x86_64-unknown-linux-musl
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ github.ref || github.run_id }}
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Build
uses: actions-rs/cargo@v1
with:
use-cross: true
command: build
args: --verbose --release --target ${{ matrix.target }}
- name: Extract changelog content
id: extract_changelog
shell: bash
run: |
version="${{ needs.setup.outputs.version }}"
echo "${{ needs.setup.outputs.info }}" > changelog_output.txt
awk "/^## \\[$version\\]/ {flag=1; next} /^## \\[/ && flag {flag=0} flag" CHANGELOG.md >> changelog_output.txt
- name: Display extracted content
run: cat changelog_output.txt
- name: Release
if: ${{ needs.setup.outputs.dry_run == 'false'}}
uses: softprops/action-gh-release@v2
with:
body_path: changelog_output.txt
deploy-to-crates-io:
needs:
- setup
- build-and-upload
name: Deploy to crates.io
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ github.ref || github.run_id }}
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/cache-cargo-install-action@v2
with:
tool: cargo-release
- name: cargo publish dry run
if: ${{ needs.setup.outputs.dry_run == 'true'}}
run: cargo publish --dry-run
- name: cargo login
run: cargo login ${{ secrets.CRATES_IO_TOKEN }}
- name: "cargo release publish"
if: ${{ needs.setup.outputs.dry_run == 'false'}}
run: |-
cargo release \
publish \
--workspace \
--all-features \
--allow-branch HEAD \
--no-confirm \
--no-verify \
--execute

View File

@ -1,5 +1,44 @@
# CHANGELOG # CHANGELOG
## [0.4.0]
## Changed
- Updated to Bevy 0.16
## [0.3.2]
## Added
- `scroll_view_node` function with default values for `ScrollView` node
- documentation to the code
## Fixed
- `ScrollableContent` default Node values fix
## [0.3.1]
## Fixed
- Size calculation missmatch [#3](https://github.com/Leinnan/bevy_simple_scroll_view/issues/3)
## [0.3.0]
## Added
- scroll to bottom and top functions for `ScrollableContent` component.
## Changed
- Updated to Bevy 0.15
## [0.2.0]
## Changed
- Updated to Bevy 0.14
## [0.1.0] ## [0.1.0]
- Initial version - Initial version

View File

@ -1,20 +1,27 @@
[package] [package]
name = "bevy_simple_scroll_view" name = "bevy_simple_scroll_view"
version = "0.1.0" version = "0.4.0"
edition = "2021" edition = "2021"
exclude = [".github/","wasm/", "record.gif"] exclude = [".github/", "wasm/", "record.gif"]
categories = ["game-development", "gui"] categories = ["game-development", "gui"]
keywords = ["bevy","ui"] keywords = ["bevy", "ui"]
repository = "https://github.com/Leinnan/bevy_simple_scroll_view" repository = "https://github.com/Leinnan/bevy_simple_scroll_view"
homepage = "https://github.com/Leinnan/bevy_simple_scroll_view" homepage = "https://github.com/Leinnan/bevy_simple_scroll_view"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
description = "Simple to use plugin implementing ScrollView into Bevy engine." description = "Simple to use plugin implementing ScrollView into Bevy engine."
[dependencies.bevy] [dependencies.bevy]
version = "0.13" version = "0.16"
default-features = false default-features = false
features = ["bevy_ui", "bevy_asset", "bevy_text"] features = ["bevy_ui", "bevy_asset", "bevy_text"]
[dev-dependencies.bevy] [dev-dependencies.bevy]
version = "0.13" version = "0.16"
default-features = true default-features = true
[features]
default = []
extra_logs = ["bevy/bevy_log"]
[lints.rust]
missing_docs = "warn"

View File

@ -36,4 +36,7 @@ Please keep PRs small and scoped to a single feature or fix.
Bevy version | crate version Bevy version | crate version
--- | --- --- | ---
0.16 | 0.4
0.15 | 0.3
0.14 | 0.2
0.13 | 0.1 0.13 | 0.1

View File

@ -1,122 +1,84 @@
#![allow(missing_docs)]
use bevy::picking::events::{Pointer, Released};
use bevy::prelude::*; use bevy::prelude::*;
use bevy_simple_scroll_view::*; use bevy_simple_scroll_view::*;
const CLR_1: Color = Color::rgb(0.168, 0.168, 0.168); const BG_COLOR: BackgroundColor = BackgroundColor(Color::srgb(0.168, 0.168, 0.168));
const CLR_2: Color = Color::rgb(0.109, 0.109, 0.109); const BG_COLOR_2: BackgroundColor = BackgroundColor(Color::srgb(0.109, 0.109, 0.109));
const CLR_3: Color = Color::rgb(0.569, 0.592, 0.647); const CLR_3: Color = Color::srgb(0.569, 0.592, 0.647);
const CLR_4: Color = Color::rgb(0.902, 0.4, 0.004); const TEXT_COLOR: TextColor = TextColor(Color::srgb(0.902, 0.4, 0.004));
fn main() { fn main() {
App::new() App::new()
.add_plugins((DefaultPlugins, ScrollViewPlugin)) .add_plugins((DefaultPlugins, ScrollViewPlugin))
.add_systems(Startup, prepare) .add_systems(Startup, setup)
.add_systems(Update, reset_scroll)
.run(); .run();
} }
fn prepare(mut commands: Commands) { fn setup(mut commands: Commands) {
commands.spawn(Camera2dBundle::default()); let base_node = Node {
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
min_width: Val::Px(200.0),
margin: UiRect::all(Val::Px(10.0)),
border: UiRect::all(Val::Px(5.0)),
padding: UiRect::all(Val::Px(15.0)),
..default()
};
commands.spawn(Camera2d);
commands commands
.spawn(NodeBundle { .spawn((
style: Style { BG_COLOR,
Node {
width: Val::Percent(100.0), width: Val::Percent(100.0),
height: Val::Percent(100.0), height: Val::Percent(100.0),
padding: UiRect::all(Val::Px(15.0)),
..default() ..default()
}, },
background_color: CLR_1.into(), ))
..default()
})
.with_children(|p| { .with_children(|p| {
p.spawn(ButtonBundle { p.spawn(Node {
style: Style { width: Val::Percent(20.0),
margin: UiRect::all(Val::Px(15.0)), margin: UiRect::all(Val::Px(15.0)),
padding: UiRect::all(Val::Px(15.0)), flex_direction: FlexDirection::Column,
max_height: Val::Px(100.0),
border: UiRect::all(Val::Px(3.0)),
align_items: AlignItems::Center,
..default()
},
background_color: CLR_2.into(),
border_color: CLR_4.into(),
..default() ..default()
}) })
.with_children(|p| { .with_children(|p| {
p.spawn(TextBundle::from_section( p.spawn((Text::new("Scroll to:"), TEXT_COLOR));
"Reset scroll", let btn = (base_node.clone(), BG_COLOR_2, Button);
TextStyle { p.spawn(btn.clone())
font_size: 25.0, .observe(scroll_to_top)
color: CLR_4, .with_child((Text::new("top"), TEXT_COLOR));
..default() p.spawn(btn)
}, .observe(scroll_to_bottom)
)); .with_child((Text::new("bottom"), TEXT_COLOR));
}); });
p.spawn(( p.spawn((
NodeBundle { Node {
style: Style { width: Val::Percent(80.0),
width: Val::Percent(80.0), margin: UiRect::all(Val::Px(15.0)),
margin: UiRect::all(Val::Px(15.0)), ..scroll_view_node()
..default()
},
background_color: CLR_2.into(),
..default()
}, },
BG_COLOR_2,
ScrollView::default(), ScrollView::default(),
)) ))
.with_children(|p| { .with_children(|p| {
p.spawn(( p.spawn(ScrollableContent::default())
NodeBundle { .with_children(|scroll_area| {
style: Style { for i in 1..21 {
flex_direction: bevy::ui::FlexDirection::Column, scroll_area
width: Val::Percent(100.0), .spawn((base_node.clone(), BorderColor(CLR_3)))
..default() .with_child((Text::new(format!("Nr {} out of 20", i)), TEXT_COLOR));
}, }
..default() });
},
ScrollableContent::default(),
))
.with_children(|scroll_area| {
for i in 0..21 {
scroll_area
.spawn(NodeBundle {
style: Style {
min_width: Val::Px(200.0),
margin: UiRect::all(Val::Px(15.0)),
border: UiRect::all(Val::Px(5.0)),
padding: UiRect::all(Val::Px(30.0)),
..default()
},
border_color: CLR_3.into(),
..default()
})
.with_children(|p| {
p.spawn(
TextBundle::from_section(
format!("Nr {}", i),
TextStyle {
font_size: 25.0,
color: CLR_3,
..default()
},
)
.with_text_justify(JustifyText::Center),
);
});
}
});
}); });
}); });
} }
fn reset_scroll( fn scroll_to_top(_t: Trigger<Pointer<Released>>, mut scroll: Single<&mut ScrollableContent>) {
q: Query<(&Button, &Interaction), Changed<Interaction>>, scroll.scroll_to_top();
mut scrolls_q: Query<&mut ScrollableContent>, }
) {
for (_, interaction) in q.iter() { fn scroll_to_bottom(_t: Trigger<Pointer<Released>>, mut scroll: Single<&mut ScrollableContent>) {
if interaction == &Interaction::Pressed { scroll.scroll_to_bottom();
for mut scroll in scrolls_q.iter_mut() {
scroll.pos_y = 0.0;
}
}
}
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 KiB

After

Width:  |  Height:  |  Size: 182 KiB

View File

@ -8,7 +8,7 @@ use bevy::{
/// A `Plugin` providing the systems and components required to make a ScrollView work. /// A `Plugin` providing the systems and components required to make a ScrollView work.
/// ///
/// # Example /// # Example
/// ``` /// ```no_run
/// use bevy::prelude::*; /// use bevy::prelude::*;
/// use bevy_simple_scroll_view::*; /// use bevy_simple_scroll_view::*;
/// ///
@ -26,6 +26,7 @@ impl Plugin for ScrollViewPlugin {
Update, Update,
( (
create_scroll_view, create_scroll_view,
update_size,
input_mouse_pressed_move, input_mouse_pressed_move,
input_touch_pressed_move, input_touch_pressed_move,
scroll_events, scroll_events,
@ -38,6 +39,7 @@ impl Plugin for ScrollViewPlugin {
/// Root component of scroll, it should have clipped style. /// Root component of scroll, it should have clipped style.
#[derive(Component, Debug, Reflect)] #[derive(Component, Debug, Reflect)]
#[require(Interaction, Node = scroll_view_node())]
pub struct ScrollView { pub struct ScrollView {
/// Field which control speed of the scrolling. /// Field which control speed of the scrolling.
/// Could be negative number to implement invert scroll /// Could be negative number to implement invert scroll
@ -47,7 +49,7 @@ pub struct ScrollView {
impl Default for ScrollView { impl Default for ScrollView {
fn default() -> Self { fn default() -> Self {
Self { Self {
scroll_speed: 200.0, scroll_speed: 1200.0,
} }
} }
} }
@ -55,69 +57,140 @@ impl Default for ScrollView {
/// Component containing offset value of the scroll container to the parent. /// Component containing offset value of the scroll container to the parent.
/// It is possible to update the field `pos_y` manually to move scrollview to desired location. /// It is possible to update the field `pos_y` manually to move scrollview to desired location.
#[derive(Component, Debug, Reflect, Default)] #[derive(Component, Debug, Reflect, Default)]
#[require(Node = scroll_content_node())]
pub struct ScrollableContent { pub struct ScrollableContent {
/// Scroll container offset to the `ScrollView`. /// Scroll container offset to the `ScrollView`.
pub pos_y: f32, pub pos_y: f32,
/// Maximum value for the scroll. It is updated automatically based on the size of the children nodes.
pub max_scroll: f32,
} }
pub fn create_scroll_view( impl ScrollableContent {
mut commands: Commands, /// Scrolls to the top of the scroll view.
mut q: Query<(Entity, &mut Style), Added<ScrollView>>, pub fn scroll_to_top(&mut self) {
) { self.pos_y = 0.0;
for (e, mut style) in q.iter_mut() { }
style.overflow = Overflow::clip(); /// Scrolls to the bottom of the scroll view.
style.align_items = AlignItems::Start; pub fn scroll_to_bottom(&mut self) {
style.align_self = AlignSelf::Stretch; self.pos_y = -self.max_scroll;
style.flex_direction = FlexDirection::Row; }
commands.entity(e).insert(Interaction::None);
/// Scrolls by a specified amount.
///
/// # Parameters
/// - `value`: The amount to scroll vertically. Positive values scroll down,
/// and negative values scroll up.
///
/// Ensures the new position is clamped between the valid scroll range.
pub fn scroll_by(&mut self, value: f32) {
self.pos_y += value;
self.pos_y = self.pos_y.clamp(-self.max_scroll, 0.);
}
}
/// Creates a default scroll view node.
///
/// This function defines the visual and layout properties of a scrollable container.
pub fn scroll_view_node() -> Node {
Node {
overflow: Overflow::clip(),
align_items: AlignItems::Start,
align_self: AlignSelf::Stretch,
flex_direction: FlexDirection::Row,
..default()
}
}
/// Creates a default scroll content node.
pub fn scroll_content_node() -> Node {
Node {
flex_direction: bevy::ui::FlexDirection::Column,
width: Val::Percent(100.0),
..default()
}
}
/// Applies the default scroll view style to newly added `ScrollView` components.
///
/// This function updates the style of all new `ScrollView` nodes with the default
/// properties defined in `scroll_view_node`.
pub fn create_scroll_view(mut q: Query<&mut Node, Added<ScrollView>>) {
let Node {
overflow,
align_items,
align_self,
flex_direction,
..
} = scroll_view_node();
for mut style in q.iter_mut() {
style.overflow = overflow;
style.align_items = align_items;
style.align_self = align_self;
style.flex_direction = flex_direction;
} }
} }
fn input_mouse_pressed_move( fn input_mouse_pressed_move(
mut motion_evr: EventReader<MouseMotion>, mut motion_evr: EventReader<MouseMotion>,
mut q: Query<(&Children, &Interaction, &Node), With<ScrollView>>, mut q: Query<(&Children, &Interaction), With<ScrollView>>,
mut content_q: Query<(&mut ScrollableContent, &Node)>, mut content_q: Query<&mut ScrollableContent>,
) { ) {
for evt in motion_evr.read() { for evt in motion_evr.read() {
for (children, &interaction, node) in q.iter_mut() { for (children, &interaction) in q.iter_mut() {
if interaction != Interaction::Pressed { if interaction != Interaction::Pressed {
continue; continue;
} }
let container_height = node.size().y; for child in children.iter() {
for &child in children.iter() { let Ok(mut scroll) = content_q.get_mut(child) else {
if let Ok(item) = content_q.get_mut(child) { continue;
let mut scroll = item.0; };
let max_scroll = (item.1.size().y - container_height).max(0.0); scroll.scroll_by(evt.delta.y);
scroll.pos_y += evt.delta.y;
scroll.pos_y = scroll.pos_y.clamp(-max_scroll, 0.);
}
} }
} }
} }
} }
fn update_size(
mut q: Query<(&Children, &ComputedNode), With<ScrollView>>,
mut content_q: Query<(&mut ScrollableContent, &ComputedNode), Changed<ComputedNode>>,
) {
for (children, scroll_view_node) in q.iter_mut() {
let container_height = scroll_view_node.size().y * scroll_view_node.inverse_scale_factor();
for child in children.iter() {
let Ok((mut scroll, node)) = content_q.get_mut(child) else {
continue;
};
scroll.max_scroll =
(node.size().y * node.inverse_scale_factor() - container_height).max(0.0);
#[cfg(feature = "extra_logs")]
info!(
"CONTAINER {}, max_scroll: {}",
container_height, scroll.max_scroll
);
}
}
}
fn input_touch_pressed_move( fn input_touch_pressed_move(
touches: Res<Touches>, touches: Res<Touches>,
mut q: Query<(&Children, &Interaction, &Node), With<ScrollView>>, mut q: Query<(&Children, &Interaction), With<ScrollView>>,
mut content_q: Query<(&mut ScrollableContent, &Node)>, mut content_q: Query<&mut ScrollableContent>,
) { ) {
for t in touches.iter() { for t in touches.iter() {
let Some(touch) = touches.get_pressed(t.id()) else { let Some(touch) = touches.get_pressed(t.id()) else {
continue; continue;
}; };
for (children, &interaction, node) in q.iter_mut() { for (children, &interaction) in q.iter_mut() {
if interaction != Interaction::Pressed { if interaction != Interaction::Pressed {
continue; continue;
} }
let container_height = node.size().y; for child in children.iter() {
for &child in children.iter() { let Ok(mut scroll) = content_q.get_mut(child) else {
if let Ok(item) = content_q.get_mut(child) { continue;
let mut scroll = item.0; };
let max_scroll = (item.1.size().y - container_height).max(0.0); scroll.scroll_by(touch.delta().y);
scroll.pos_y += touch.delta().y;
scroll.pos_y = scroll.pos_y.clamp(-max_scroll, 0.);
}
} }
} }
} }
@ -125,38 +198,36 @@ fn input_touch_pressed_move(
fn scroll_events( fn scroll_events(
mut scroll_evr: EventReader<MouseWheel>, mut scroll_evr: EventReader<MouseWheel>,
mut q: Query<(&Children, &Interaction, &ScrollView, &Node), With<ScrollView>>, mut q: Query<(&Children, &Interaction, &ScrollView), With<ScrollView>>,
time: Res<Time>, time: Res<Time>,
mut content_q: Query<(&mut ScrollableContent, &Node)>, mut content_q: Query<&mut ScrollableContent>,
) { ) {
use bevy::input::mouse::MouseScrollUnit; use bevy::input::mouse::MouseScrollUnit;
for ev in scroll_evr.read() { for ev in scroll_evr.read() {
for (children, &interaction, scroll_view, node) in q.iter_mut() { for (children, &interaction, scroll_view) in q.iter_mut() {
if interaction != Interaction::Hovered {
continue;
}
let y = match ev.unit { let y = match ev.unit {
MouseScrollUnit::Line => { MouseScrollUnit::Line => {
ev.y * time.delta().as_secs_f32() * scroll_view.scroll_speed ev.y * time.delta().as_secs_f32() * scroll_view.scroll_speed
} }
MouseScrollUnit::Pixel => ev.y, MouseScrollUnit::Pixel => ev.y,
}; };
if interaction != Interaction::Hovered { #[cfg(feature = "extra_logs")]
continue; info!("Scroolling by {:#?}: {} movement", ev.unit, y);
}
let container_height = node.size().y;
for &child in children.iter() { for child in children.iter() {
if let Ok(item) = content_q.get_mut(child) { let Ok(mut scroll) = content_q.get_mut(child) else {
let y = y * time.delta().as_secs_f32() * scroll_view.scroll_speed; continue;
let mut scroll = item.0; };
let max_scroll = (item.1.size().y - container_height).max(0.0); scroll.scroll_by(y);
scroll.pos_y += y;
scroll.pos_y = scroll.pos_y.clamp(-max_scroll, 0.);
}
} }
} }
} }
} }
fn scroll_update(mut q: Query<(&ScrollableContent, &mut Style), Changed<ScrollableContent>>) { fn scroll_update(mut q: Query<(&ScrollableContent, &mut Node), Changed<ScrollableContent>>) {
for (scroll, mut style) in q.iter_mut() { for (scroll, mut style) in q.iter_mut() {
style.top = Val::Px(scroll.pos_y); style.top = Val::Px(scroll.pos_y);
} }