Implement efficent scrolling of large content (#457)
This commit is contained in:
parent
2cdd90b111
commit
6468b2b84e
6 changed files with 209 additions and 16 deletions
|
@ -12,6 +12,7 @@ NOTE: [`eframe`](eframe/CHANGELOG.md), [`egui_web`](egui_web/CHANGELOG.md) and [
|
||||||
* Add right and bottom panels (`SidePanel::right` and `Panel::bottom`).
|
* Add right and bottom panels (`SidePanel::right` and `Panel::bottom`).
|
||||||
* Add resizable panels.
|
* Add resizable panels.
|
||||||
* Add an option to overwrite frame of a `Panel`.
|
* Add an option to overwrite frame of a `Panel`.
|
||||||
|
* Add `ScrollArea::show_rows` for efficient scrolling of huge UI:s.
|
||||||
* Add `Style::override_text_style` to easily change the text style of everything in a `Ui` (or globally).
|
* Add `Style::override_text_style` to easily change the text style of everything in a `Ui` (or globally).
|
||||||
* You can now change `TextStyle` on checkboxes, radio buttons and `SelectableLabel`.
|
* You can now change `TextStyle` on checkboxes, radio buttons and `SelectableLabel`.
|
||||||
* Add support for [cint](https://crates.io/crates/cint) under `cint` feature.
|
* Add support for [cint](https://crates.io/crates/cint) under `cint` feature.
|
||||||
|
|
|
@ -263,9 +263,9 @@ For "atomic" widgets (e.g. a button) `egui` knows the size before showing it, so
|
||||||
#### CPU usage
|
#### CPU usage
|
||||||
Since an immediate mode GUI does a full layout each frame, the layout code needs to be quick. If you have a very complex GUI this can tax the CPU. In particular, having a very large UI in a scroll area (with very long scrollback) can be slow, as the content needs to be layed out each frame.
|
Since an immediate mode GUI does a full layout each frame, the layout code needs to be quick. If you have a very complex GUI this can tax the CPU. In particular, having a very large UI in a scroll area (with very long scrollback) can be slow, as the content needs to be layed out each frame.
|
||||||
|
|
||||||
If you design the GUI with this in mind and refrain from huge scroll areas then the performance hit is generally pretty small. For most cases you can expect `egui` to take up 1-2 ms per frame, but `egui` still has a lot of room for optimization (it's not something I've focused on yet). You can also set up `egui` to only repaint when there is interaction (e.g. mouse movement).
|
If you design the GUI with this in mind and refrain from huge scroll areas (or only lay out the part that is in view) then the performance hit is generally pretty small. For most cases you can expect `egui` to take up 1-2 ms per frame, but `egui` still has a lot of room for optimization (it's not something I've focused on yet). You can also set up `egui` to only repaint when there is interaction (e.g. mouse movement).
|
||||||
|
|
||||||
If your GUI is highly interactive, then immediate mode may actually be more performant compared to retained mode. Go to any web page and resize the browser window, and you'll notice that the browser is very slow to do the layout and eats a lot of CPU doing it. Resize a window in `egui` by contrast, and you'll get smooth 60 FPS for no extra CPU cost.
|
If your GUI is highly interactive, then immediate mode may actually be more performant compared to retained mode. Go to any web page and resize the browser window, and you'll notice that the browser is very slow to do the layout and eats a lot of CPU doing it. Resize a window in `egui` by contrast, and you'll get smooth 60 FPS at no extra CPU cost.
|
||||||
|
|
||||||
|
|
||||||
#### IDs
|
#### IDs
|
||||||
|
|
|
@ -84,6 +84,9 @@ struct Prepared {
|
||||||
always_show_scroll: bool,
|
always_show_scroll: bool,
|
||||||
inner_rect: Rect,
|
inner_rect: Rect,
|
||||||
content_ui: Ui,
|
content_ui: Ui,
|
||||||
|
/// Relative coordinates: the offset and size of the view of the inner UI.
|
||||||
|
/// `viewport.min == ZERO` means we scrolled to the top.
|
||||||
|
viewport: Rect,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScrollArea {
|
impl ScrollArea {
|
||||||
|
@ -139,6 +142,8 @@ impl ScrollArea {
|
||||||
content_clip_rect.max.x = ui.clip_rect().max.x - current_scroll_bar_width; // Nice handling of forced resizing beyond the possible
|
content_clip_rect.max.x = ui.clip_rect().max.x - current_scroll_bar_width; // Nice handling of forced resizing beyond the possible
|
||||||
content_ui.set_clip_rect(content_clip_rect);
|
content_ui.set_clip_rect(content_clip_rect);
|
||||||
|
|
||||||
|
let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
|
||||||
|
|
||||||
Prepared {
|
Prepared {
|
||||||
id,
|
id,
|
||||||
state,
|
state,
|
||||||
|
@ -146,12 +151,69 @@ impl ScrollArea {
|
||||||
always_show_scroll,
|
always_show_scroll,
|
||||||
inner_rect,
|
inner_rect,
|
||||||
content_ui,
|
content_ui,
|
||||||
|
viewport,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Show the `ScrollArea`, and add the contents to the viewport.
|
||||||
|
///
|
||||||
|
/// If the inner area can be very long, consider using [`Self::show_rows`] instead.
|
||||||
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> R {
|
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> R {
|
||||||
|
self.show_viewport(ui, |ui, _viewport| add_contents(ui))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Efficiently show only the visible part of a large number of rows.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # let ui = &mut egui::Ui::__test();
|
||||||
|
/// let text_style = egui::TextStyle::Body;
|
||||||
|
/// let row_height = ui.fonts()[text_style].row_height();
|
||||||
|
/// // let row_height = ui.spacing().interact_size.y; // if you are adding buttons instead of labels.
|
||||||
|
/// let num_rows = 10_000;
|
||||||
|
/// egui::ScrollArea::auto_sized().show_rows(ui, row_height, num_rows, |ui, row_range| {
|
||||||
|
/// for row in row_range {
|
||||||
|
/// let text = format!("Row {}/{}", row + 1, num_rows);
|
||||||
|
/// ui.label(text);
|
||||||
|
/// }
|
||||||
|
/// });
|
||||||
|
pub fn show_rows<R>(
|
||||||
|
self,
|
||||||
|
ui: &mut Ui,
|
||||||
|
row_height_sans_spacing: f32,
|
||||||
|
num_rows: usize,
|
||||||
|
add_contents: impl FnOnce(&mut Ui, std::ops::Range<usize>) -> R,
|
||||||
|
) -> R {
|
||||||
|
let spacing = ui.spacing().item_spacing;
|
||||||
|
let row_height_with_spacing = row_height_sans_spacing + spacing.y;
|
||||||
|
self.show_viewport(ui, |ui, viewport| {
|
||||||
|
ui.set_height((row_height_with_spacing * num_rows as f32 - spacing.y).at_least(0.0));
|
||||||
|
|
||||||
|
let min_row = (viewport.min.y / row_height_with_spacing)
|
||||||
|
.floor()
|
||||||
|
.at_least(0.0) as usize;
|
||||||
|
let max_row = (viewport.max.y / row_height_with_spacing).ceil() as usize + 1;
|
||||||
|
let max_row = max_row.at_most(num_rows);
|
||||||
|
|
||||||
|
let y_min = ui.max_rect().top() + min_row as f32 * row_height_with_spacing;
|
||||||
|
let y_max = ui.max_rect().top() + max_row as f32 * row_height_with_spacing;
|
||||||
|
let mut viewport_ui = ui.child_ui(
|
||||||
|
Rect::from_x_y_ranges(ui.max_rect().x_range(), y_min..=y_max),
|
||||||
|
*ui.layout(),
|
||||||
|
);
|
||||||
|
|
||||||
|
viewport_ui.skip_ahead_auto_ids(min_row); // Make sure we get consistent IDs.
|
||||||
|
|
||||||
|
add_contents(&mut viewport_ui, min_row..max_row)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This can be used to only paint the visible part of the contents.
|
||||||
|
///
|
||||||
|
/// `add_contents` is past the viewport, which is the relative view of the content.
|
||||||
|
/// So if the passed rect has min = zero, then show the top left content (the user has not scrolled).
|
||||||
|
pub fn show_viewport<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui, Rect) -> R) -> R {
|
||||||
let mut prepared = self.begin(ui);
|
let mut prepared = self.begin(ui);
|
||||||
let ret = add_contents(&mut prepared.content_ui);
|
let ret = add_contents(&mut prepared.content_ui, prepared.viewport);
|
||||||
prepared.end(ui);
|
prepared.end(ui);
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
|
@ -166,6 +228,7 @@ impl Prepared {
|
||||||
always_show_scroll,
|
always_show_scroll,
|
||||||
mut current_scroll_bar_width,
|
mut current_scroll_bar_width,
|
||||||
content_ui,
|
content_ui,
|
||||||
|
viewport: _,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let content_size = content_ui.min_size();
|
let content_size = content_ui.min_size();
|
||||||
|
|
|
@ -254,6 +254,7 @@ pub struct Grid {
|
||||||
min_row_height: Option<f32>,
|
min_row_height: Option<f32>,
|
||||||
max_cell_size: Vec2,
|
max_cell_size: Vec2,
|
||||||
spacing: Option<Vec2>,
|
spacing: Option<Vec2>,
|
||||||
|
start_row: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Grid {
|
impl Grid {
|
||||||
|
@ -266,6 +267,7 @@ impl Grid {
|
||||||
min_row_height: None,
|
min_row_height: None,
|
||||||
max_cell_size: Vec2::INFINITY,
|
max_cell_size: Vec2::INFINITY,
|
||||||
spacing: None,
|
spacing: None,
|
||||||
|
start_row: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -304,6 +306,13 @@ impl Grid {
|
||||||
self.spacing = Some(spacing.into());
|
self.spacing = Some(spacing.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Change which row number the grid starts on.
|
||||||
|
/// This can be useful when you have a large `Grid` inside of [`ScrollArea::show_rows`].
|
||||||
|
pub fn start_row(mut self, start_row: usize) -> Self {
|
||||||
|
self.start_row = start_row;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Grid {
|
impl Grid {
|
||||||
|
@ -315,6 +324,7 @@ impl Grid {
|
||||||
min_row_height,
|
min_row_height,
|
||||||
max_cell_size,
|
max_cell_size,
|
||||||
spacing,
|
spacing,
|
||||||
|
start_row,
|
||||||
} = self;
|
} = self;
|
||||||
let min_col_width = min_col_width.unwrap_or_else(|| ui.spacing().interact_size.x);
|
let min_col_width = min_col_width.unwrap_or_else(|| ui.spacing().interact_size.x);
|
||||||
let min_row_height = min_row_height.unwrap_or_else(|| ui.spacing().interact_size.y);
|
let min_row_height = min_row_height.unwrap_or_else(|| ui.spacing().interact_size.y);
|
||||||
|
@ -331,6 +341,7 @@ impl Grid {
|
||||||
spacing,
|
spacing,
|
||||||
min_cell_size: vec2(min_col_width, min_row_height),
|
min_cell_size: vec2(min_col_width, min_row_height),
|
||||||
max_cell_size,
|
max_cell_size,
|
||||||
|
row: start_row,
|
||||||
..GridLayout::new(ui, id)
|
..GridLayout::new(ui, id)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -401,12 +401,18 @@ impl Ui {
|
||||||
self.set_max_width(*width.end());
|
self.set_max_width(*width.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `ui.set_width_range(width);` is equivalent to `ui.set_min_width(width); ui.set_max_width(width);`.
|
/// Set both the minimum and maximum width.
|
||||||
pub fn set_width(&mut self, width: f32) {
|
pub fn set_width(&mut self, width: f32) {
|
||||||
self.set_min_width(width);
|
self.set_min_width(width);
|
||||||
self.set_max_width(width);
|
self.set_max_width(width);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set both the minimum and maximum height.
|
||||||
|
pub fn set_height(&mut self, height: f32) {
|
||||||
|
self.set_min_height(height);
|
||||||
|
self.set_max_height(height);
|
||||||
|
}
|
||||||
|
|
||||||
/// Ensure we are big enough to contain the given x-coordinate.
|
/// Ensure we are big enough to contain the given x-coordinate.
|
||||||
/// This is sometimes useful to expand an ui to stretch to a certain place.
|
/// This is sometimes useful to expand an ui to stretch to a certain place.
|
||||||
pub fn expand_to_include_x(&mut self, x: f32) {
|
pub fn expand_to_include_x(&mut self, x: f32) {
|
||||||
|
@ -473,6 +479,10 @@ impl Ui {
|
||||||
{
|
{
|
||||||
Id::new(self.next_auto_id_source).with(id_source)
|
Id::new(self.next_auto_id_source).with(id_source)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn skip_ahead_auto_ids(&mut self, count: usize) {
|
||||||
|
self.next_auto_id_source = self.next_auto_id_source.wrapping_add(count as u64);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # Interaction
|
/// # Interaction
|
||||||
|
|
|
@ -1,24 +1,27 @@
|
||||||
use egui::{color::*, *};
|
use egui::{color::*, *};
|
||||||
|
|
||||||
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||||
#[cfg_attr(feature = "persistence", serde(default))]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
#[derive(PartialEq)]
|
enum ScrollDemo {
|
||||||
pub struct Scrolling {
|
ScrollTo,
|
||||||
track_item: usize,
|
ManyLines,
|
||||||
tack_item_align: Align,
|
LargeCanvas,
|
||||||
offset: f32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Scrolling {
|
impl Default for ScrollDemo {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self::ScrollTo
|
||||||
track_item: 25,
|
|
||||||
tack_item_align: Align::Center,
|
|
||||||
offset: 0.0,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
#[cfg_attr(feature = "persistence", serde(default))]
|
||||||
|
#[derive(Default, PartialEq)]
|
||||||
|
pub struct Scrolling {
|
||||||
|
demo: ScrollDemo,
|
||||||
|
scroll_to: ScrollTo,
|
||||||
|
}
|
||||||
|
|
||||||
impl super::Demo for Scrolling {
|
impl super::Demo for Scrolling {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
"↕ Scrolling"
|
"↕ Scrolling"
|
||||||
|
@ -36,6 +39,111 @@ impl super::Demo for Scrolling {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl super::View for Scrolling {
|
impl super::View for Scrolling {
|
||||||
|
fn ui(&mut self, ui: &mut Ui) {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.selectable_value(&mut self.demo, ScrollDemo::ScrollTo, "Scroll to");
|
||||||
|
ui.selectable_value(
|
||||||
|
&mut self.demo,
|
||||||
|
ScrollDemo::ManyLines,
|
||||||
|
"Scroll a lot of lines",
|
||||||
|
);
|
||||||
|
ui.selectable_value(
|
||||||
|
&mut self.demo,
|
||||||
|
ScrollDemo::LargeCanvas,
|
||||||
|
"Scroll a large canvas",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
ui.separator();
|
||||||
|
match self.demo {
|
||||||
|
ScrollDemo::ScrollTo => {
|
||||||
|
self.scroll_to.ui(ui);
|
||||||
|
}
|
||||||
|
ScrollDemo::ManyLines => {
|
||||||
|
huge_content_lines(ui);
|
||||||
|
}
|
||||||
|
ScrollDemo::LargeCanvas => {
|
||||||
|
huge_content_painter(ui);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn huge_content_lines(ui: &mut egui::Ui) {
|
||||||
|
ui.label(
|
||||||
|
"A lot of rows, but only the visible ones are layed out, so performance is still good:",
|
||||||
|
);
|
||||||
|
ui.add_space(4.0);
|
||||||
|
|
||||||
|
let text_style = TextStyle::Body;
|
||||||
|
let row_height = ui.fonts()[text_style].row_height();
|
||||||
|
let num_rows = 10_000;
|
||||||
|
ScrollArea::auto_sized().show_rows(ui, row_height, num_rows, |ui, row_range| {
|
||||||
|
for row in row_range {
|
||||||
|
let text = format!("This is row {}/{}", row + 1, num_rows);
|
||||||
|
ui.label(text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn huge_content_painter(ui: &mut egui::Ui) {
|
||||||
|
// This is similar to the other demo, but is fully manual, for when you want to do custom painting.
|
||||||
|
ui.label("A lot of rows, but only the visible ones are painted, so performance is still good:");
|
||||||
|
ui.add_space(4.0);
|
||||||
|
|
||||||
|
let text_style = TextStyle::Body;
|
||||||
|
let row_height = ui.fonts()[text_style].row_height() + ui.spacing().item_spacing.y;
|
||||||
|
let num_rows = 10_000;
|
||||||
|
|
||||||
|
ScrollArea::auto_sized().show_viewport(ui, |ui, viewport| {
|
||||||
|
ui.set_height(row_height * num_rows as f32);
|
||||||
|
|
||||||
|
let first_item = (viewport.min.y / row_height).floor().at_least(0.0) as usize;
|
||||||
|
let last_item = (viewport.max.y / row_height).ceil() as usize + 1;
|
||||||
|
let last_item = last_item.at_most(num_rows);
|
||||||
|
|
||||||
|
for i in first_item..last_item {
|
||||||
|
let indentation = (i % 100) as f32;
|
||||||
|
let x = ui.min_rect().left() + indentation;
|
||||||
|
let y = ui.min_rect().top() + i as f32 * row_height;
|
||||||
|
let text = format!(
|
||||||
|
"This is row {}/{}, indented by {} pixels",
|
||||||
|
i + 1,
|
||||||
|
num_rows,
|
||||||
|
indentation
|
||||||
|
);
|
||||||
|
ui.painter().text(
|
||||||
|
pos2(x, y),
|
||||||
|
Align2::LEFT_TOP,
|
||||||
|
text,
|
||||||
|
text_style,
|
||||||
|
ui.visuals().text_color(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
#[cfg_attr(feature = "persistence", serde(default))]
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
struct ScrollTo {
|
||||||
|
track_item: usize,
|
||||||
|
tack_item_align: Align,
|
||||||
|
offset: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ScrollTo {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
track_item: 25,
|
||||||
|
tack_item_align: Align::Center,
|
||||||
|
offset: 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::View for ScrollTo {
|
||||||
fn ui(&mut self, ui: &mut Ui) {
|
fn ui(&mut self, ui: &mut Ui) {
|
||||||
ui.label("This shows how you can scroll to a specific item or pixel offset");
|
ui.label("This shows how you can scroll to a specific item or pixel offset");
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue