Skip to content

Simple stresstest for Virtual Scrolling #985

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 90 additions & 26 deletions masonry/src/widgets/virtual_scroll.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use std::{collections::HashMap, ops::Range};
use vello::kurbo::{Point, Size, Vec2};

use crate::core::{
BoxConstraints, FromDynWidget, PointerEvent, PropertiesMut, PropertiesRef, ScrollDelta,
TextEvent, Widget, WidgetMut, WidgetPod,
BoxConstraints, EventCtx, FromDynWidget, PointerEvent, PropertiesMut, PropertiesRef,
ScrollDelta, TextEvent, Widget, WidgetMut, WidgetPod,
keyboard::{Key, KeyState, NamedKey},
};

Expand Down Expand Up @@ -225,6 +225,9 @@ pub struct VirtualScroll<W: Widget + FromDynWidget + ?Sized> {
warned_not_dense: bool,
/// We don't want to spam warnings about missing an action, but we want the user to be aware of it.
missed_actions_count: u32,

/// The amount to scroll by in each frame, intended for loose benchmarking.
scroll_per_frame: Option<f64>,
}

impl<W: Widget + FromDynWidget + ?Sized> std::fmt::Debug for VirtualScroll<W> {
Expand All @@ -240,6 +243,7 @@ impl<W: Widget + FromDynWidget + ?Sized> std::fmt::Debug for VirtualScroll<W> {
.field("mean_item_height", &self.mean_item_height)
.field("anchor_height", &self.anchor_height)
.field("warned_not_dense", &self.warned_not_dense)
.field("scroll_per_frame", &self.scroll_per_frame)
.finish()
}
}
Expand All @@ -266,6 +270,7 @@ impl<W: Widget + FromDynWidget + ?Sized> VirtualScroll<W> {
mean_item_height: DEFAULT_MEAN_ITEM_HEIGHT,
anchor_height: DEFAULT_MEAN_ITEM_HEIGHT,
warned_not_dense: false,
scroll_per_frame: None,
}
}

Expand All @@ -284,6 +289,15 @@ impl<W: Widget + FromDynWidget + ?Sized> VirtualScroll<W> {
self
}

/// Set the number of pixels to scroll in each frame.
///
/// This is intended to be used only for benchmarking, as a more
/// comprehensive animation system is not yet in place.
pub fn with_scroll_per_frame(mut self, amount: Option<f64>) -> Self {
self.scroll_per_frame = amount;
self
}

fn validate_valid_range(&mut self) {
if self.valid_range.end < self.valid_range.start {
debug_panic!(
Expand Down Expand Up @@ -412,6 +426,16 @@ impl<W: Widget + FromDynWidget + ?Sized> VirtualScroll<W> {
this.ctx.request_layout();
}

/// Set the number of pixels to scroll in each frame.
///
/// This is intended to be used only for benchmarking, as a more
/// comprehensive animation system is not yet in place.
/// Runtime equivalent of [`with_scroll_per_frame`](Self::with_scroll_per_frame).
pub fn set_scroll_per_frame(this: &mut WidgetMut<'_, Self>, amount: Option<f64>) {
this.widget.scroll_per_frame = amount;
this.ctx.request_anim_frame();
}

/// Forcefully align the top of the item at `idx` with the top of the
/// virtual scroll area.
///
Expand All @@ -425,23 +449,31 @@ impl<W: Widget + FromDynWidget + ?Sized> VirtualScroll<W> {
this.ctx.request_layout();
}

fn post_scroll(&mut self, ctx: &mut crate::core::EventCtx<'_>) {
/// Operations to be ran after scrolling in response to an event.
fn post_scroll_in_event(&mut self, ctx: &mut EventCtx) {
if self.post_scroll(ctx.size().height) {
ctx.request_layout();
} else {
ctx.request_compose();
}
}

/// Operations to be ran after a scrolling.
///
/// If this returns true, a layout is required; otherwise, a compose must be requested.
#[must_use]
fn post_scroll(&mut self, viewport_height: f64) -> bool {
// We only lock scrolling if we're *exactly* at the end of the range, because
// if the valid range has changed "during" an active scroll, we still want to handle
// that scroll (specifically, in case it happens to scroll us back into the active
// range "naturally")
if self.anchor_index + 1 == self.valid_range.end {
self.cap_scroll_range_down(self.anchor_height, ctx.size().height);
self.cap_scroll_range_down(self.anchor_height, viewport_height);
}
if self.anchor_index == self.valid_range.start {
self.cap_scroll_range_up();
}
if self.scroll_offset_from_anchor < 0.
|| self.scroll_offset_from_anchor >= self.anchor_height
{
ctx.request_layout();
}
ctx.request_compose();
self.scroll_offset_from_anchor < 0. || self.scroll_offset_from_anchor >= self.anchor_height
}

/// Lock scrolling so that:
Expand Down Expand Up @@ -471,7 +503,7 @@ const DEFAULT_MEAN_ITEM_HEIGHT: f64 = 60.;
impl<W: Widget + FromDynWidget + ?Sized> Widget for VirtualScroll<W> {
fn on_pointer_event(
&mut self,
ctx: &mut crate::core::EventCtx,
ctx: &mut EventCtx,
_props: &mut PropertiesMut<'_>,
event: &PointerEvent,
) {
Expand All @@ -483,15 +515,15 @@ impl<W: Widget + FromDynWidget + ?Sized> Widget for VirtualScroll<W> {
_ => 0.0,
};
self.scroll_offset_from_anchor += delta;
self.post_scroll(ctx);
self.post_scroll_in_event(ctx);
}
_ => (),
}
}

fn on_text_event(
&mut self,
ctx: &mut crate::core::EventCtx,
ctx: &mut EventCtx,
_props: &mut PropertiesMut<'_>,
event: &TextEvent,
) {
Expand All @@ -505,11 +537,11 @@ impl<W: Widget + FromDynWidget + ?Sized> Widget for VirtualScroll<W> {
let delta = 20000.;
if matches!(key_event.key, Key::Named(NamedKey::PageDown)) {
self.scroll_offset_from_anchor += delta;
self.post_scroll(ctx);
self.post_scroll_in_event(ctx);
}
if matches!(key_event.key, Key::Named(NamedKey::PageUp)) {
self.scroll_offset_from_anchor -= delta;
self.post_scroll(ctx);
self.post_scroll_in_event(ctx);
}
}
}
Expand All @@ -519,7 +551,7 @@ impl<W: Widget + FromDynWidget + ?Sized> Widget for VirtualScroll<W> {

fn on_access_event(
&mut self,
_ctx: &mut crate::core::EventCtx,
_ctx: &mut EventCtx,
_props: &mut PropertiesMut<'_>,
_event: &crate::core::AccessEvent,
) {
Expand All @@ -535,12 +567,16 @@ impl<W: Widget + FromDynWidget + ?Sized> Widget for VirtualScroll<W> {

fn update(
&mut self,
_ctx: &mut crate::core::UpdateCtx,
ctx: &mut crate::core::UpdateCtx,
_props: &mut PropertiesMut<'_>,
event: &crate::core::Update,
) {
match event {
crate::core::Update::WidgetAdded => {}
crate::core::Update::WidgetAdded => {
if self.scroll_per_frame.is_some() {
ctx.request_anim_frame();
}
}
crate::core::Update::DisabledChanged(_) => {}
crate::core::Update::StashedChanged(_) => {}
crate::core::Update::RequestPanToChild(_rect) => {} // TODO,
Expand All @@ -554,12 +590,37 @@ impl<W: Widget + FromDynWidget + ?Sized> Widget for VirtualScroll<W> {
}
}

fn on_anim_frame(
&mut self,
ctx: &mut crate::core::UpdateCtx,
_props: &mut PropertiesMut<'_>,
_interval: u64,
) {
if let Some(scroll_per_frame) = self.scroll_per_frame {
// tracing::info!(
// "Virtual Scroll Frame time: {:.1?}",
// Duration::from_nanos(_interval)
// );
ctx.request_anim_frame();
// Note: This is the reason that `post_scroll` doesn't just accept an `UpdateCtx`.
// Ideally, there'd be some shared, standard way to request updates.
self.scroll_offset_from_anchor += scroll_per_frame;
// TODO: This is self.post_scroll, but with this `UpdateCtx` instead of
// with `EventCtx`. This is a really poor thing for abstraction
if self.post_scroll(ctx.size().height) {
ctx.request_layout();
} else {
ctx.request_compose();
}
}
}

fn layout(
&mut self,
ctx: &mut crate::core::LayoutCtx,
_props: &mut PropertiesMut<'_>,
bc: &crate::core::BoxConstraints,
) -> vello::kurbo::Size {
bc: &BoxConstraints,
) -> Size {
let viewport_size = bc.max();
ctx.set_clip_path(viewport_size.to_rect());
let child_bc = BoxConstraints::new(
Expand Down Expand Up @@ -705,11 +766,11 @@ impl<W: Widget + FromDynWidget + ?Sized> Widget for VirtualScroll<W> {
}

// Load a page and a half above the screen
let cutoff_up = viewport_size.height * 1.5;
let cutoff_up = viewport_size.height * 0.1;
// Load a page and a half below the screen (note that this cutoff "includes" the screen)
// We also need to allow scrolling *at least* to the top of the current anchor; therefore, we load items sufficiently
// that scrolling the bottom of the anchor to the top of the screen, we still have the desired margin
let cutoff_down = viewport_size.height * 2.5 + self.anchor_height;
let cutoff_down = viewport_size.height * 1.1 + self.anchor_height;

let mut item_crossing_top = None;
let mut item_crossing_bottom = self.active_range.start;
Expand Down Expand Up @@ -823,7 +884,7 @@ impl<W: Widget + FromDynWidget + ?Sized> Widget for VirtualScroll<W> {
fn compose(&mut self, ctx: &mut crate::core::ComposeCtx) {
let translation = Vec2 {
x: 0.,
y: -self.scroll_offset_from_anchor,
y: self.scroll_offset_from_anchor,
};
for idx in self.active_range.clone() {
if let Some(child) = self.items.get_mut(&idx) {
Expand Down Expand Up @@ -912,9 +973,12 @@ impl<W: Widget + FromDynWidget + ?Sized> Widget for VirtualScroll<W> {
/// }
/// ```
/// as an iterator
#[allow(
dead_code,
reason = "Plan to expose this publicly in `VirtualScrollAction`, keep its tests around"
#[cfg_attr(
not(test),
expect(
dead_code,
reason = "Plan to expose this publicly in `VirtualScrollAction`, keep its tests around"
)
)]
fn opt_iter_difference(
old_range: &Range<i64>,
Expand Down
19 changes: 19 additions & 0 deletions masonry_winit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ targets = []
# rustdoc-scrape-examples tracking issue https://github.com/rust-lang/rust/issues/88791
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]

# This makes the examples discoverable to (e.g.) Android GPU inspector without needing to provide the full name manually.
# Do not use when releasing a production app.
[package.metadata.android.application]
debuggable = true

[features]
default = []
# Enables tracing using tracy if the default Masonry tracing is used.
Expand All @@ -38,6 +43,9 @@ wgpu-profiler = { optional = true, version = "0.22.0", default-features = false
[target.'cfg(target_arch = "wasm32")'.dependencies]
web-time.workspace = true

[target.'cfg(target_os = "android")'.dev-dependencies]
winit = { features = ["android-native-activity"], workspace = true }

[dev-dependencies]
parley.workspace = true
smallvec.workspace = true
Expand All @@ -58,3 +66,14 @@ name = "calc_masonry"
# This actually enables scraping for all examples, not just this one.
# However it is possible to set doc-scrape-examples to false for other specific examples.
doc-scrape-examples = true

[[example]]
name = "virtual_stresstest"

# Also add to ANDROID_TARGETS in .github/ci.yml if adding a new Android example
[[example]]
# A custom example target which uses the same `mason.rs` file but for android
name = "virtual_stresstest_android"
path = "examples/virtual_stresstest.rs"
# cdylib is required for cargo-apk
crate-type = ["cdylib"]
Loading
Loading