Open dignifiedquire opened 2 years ago
The only way to support this is if you know the heights of the elements beforehand. For instance, if you are showing a bunch of images of different sizes, you will (presumably) know they size of each image beforehand. Or you lay out your widgets once and then store the bounds of each in a vector (i.e. the top/bottom for vertical scrolling). You can then use ScrollArea::show_viewport
and do a binary search in your vector to figure out which elements need to be rendered. A helper for such a thing could potentially be created and added to egui
. Do you feel like working on it?
The challenge I have is that (a) I can't load all items from the database due to performance issues and (b) because there is variable sized text in there, I don't know how many rows to calculate without laying it out first.
For context, this is the messages view for a chat application.
It would be totally acceptable if the scrollbar is "wrong" and I just load elements as I reach the start/end of the scroll area, and the srollbar adjusts accordingly to what is now rendered. But I am not sure how I could achieve that either with the current setup.
Discord only shows a few messages at a time. And when you scroll too far back, it gives you a button to manually press asking whether you want to load previous messages. Same with reddit. You could try to batch the messages too like Google search results or other forums and only have like 40 or so messages at a time in scroll area.
@coderedart yes that’s what I am aiming for, just without manually pressing a button, but rather through scrolling.
let count = my_things_to_draw.len();
egui::ScrollArea::vertical().auto_shrink([false;2]).show_indexed_viewport(ui, count, |ui, viewport|{
let max_height = viewport.height();
let mut used_rect = egui::Rect::NOTHING;
for drawable in my_things_to_draw.iter().skip(viewport.start_index()) {
used_rect = used_rect.union(drawable.draw(ui));
if used_rect.height() > max_height {
break;
}
}
ui.allocate_rect(used_rect, egui::Sense::hover());
});
based on the examples I came up with this idea show_indexed_viewport
with start_index
on the viewport.
using usize
instead of f32
would allow easy implementation on the callers side, and only potential issue would be if single item would not fit to the view, but for sizes up to 8 388 607
it can solved by using f32
internally and apply some relative shift (that part would need additional function to define a size of an first item).
@emilk the main page state:
Extensible: easy to write your own widgets for egui
but when I am trying to implement item scrolling area, I am hitting private functions: ui.ctx().frame_state()
, and ui.advance_cursor_after_rect(...)
, is that intended? Are libraries supposed to use different functions? If so which?
Thanks in advance for the answer.
@xNxExOx: some things should probably be made pub
. Open a PR
…but also: containers, such as scroll areas, are not always easy to implement in egui :)
ok I will take another look at it and open PR when if I have something (if I figure how these scroll bars work)
I decided to do it differently this time, and write it within the repo, so no issues with private types and functions this time.
I have it almost working, but I am quite stuck on meaning content_is_too_small in relation to show_scroll_this_frame
Does content_is_too_small
means that the content is bigger than the area it is shown on? (That is only explanation that worked for me, in which case I think it should be called content_is_too_large
instead)
But at this point I do not think I will make it as far as PR, because I am missing few important details 😞
Not finished:
content_is_too_small
calculation and related show_scroll_this_frame
auto_shrink
is disabled for the axis with scroll barfraction
offset when scrolling to new itemScrollArea
so would be probably worth considering sharing the logic instead of duplicating for the final code.What works:
indent
s and inside button
and N label
s (N is index in the array multiplied by 2 plus 1 to include label
0) so over 1_000_000 label
s, the speed difference is ~4s per frame vs 300µs-25ms depending on the position (and that 25 ms is drawing 4001 label
s, each calling format!("{}", i)
)If you have any questions why I decided to do some of the changes the way I did, feel free to ask. If you have any suggestions how should I continue (what to look at, to fix the issues listed above) I am open to suggestions. If you think it is worth adding with so many TODOs feel free to do so. (I am kind of too perfectionist, to even consider PR at this stage)
@dignifiedquire can you try ItemScrollArea
from my PR? I fix the scrolling to not be so jumpy (in most cases).
Oops, yes it seems content_is_too_small
is named the opposite of what it means 🤦. Will fix
Thanks for confirming @emilk that it was just wrongly named, it was really confusing at first, but then I figured out, that it is either wrong name, or some crazy inversion of logic to referring to the area being to small for the content.
I updated the name in my PR too.
@xNxExOx thanks, will try it out tomorrow
@dignifiedquire how well it worked for your use case?
It renders similarly for the moment, current challenges are
stick_to_bottom
doesn't seem to be workingWhat I am unclear about is, how do I do dynamic loading, is the idea to give it the total number of known items and fetch them when the show_items
closure is called?
stick_to_bottom
I did not test that. (I use stick to top) I will take a look.Or for your specific case you can just load lets say 100 messages, and scroll to the end, if new message appear, you load them it will automatically scroll to it, and check the response after a draw, if state.offset.y == 0.0 you load another 100 messages before, no need for new functionality I think.
Dropping it here for visibility: egui_extras
uses a pretty simple API for heterogeneously sized rows: https://docs.rs/egui_extras/latest/egui_extras/struct.TableBody.html#method.heterogeneous_rows. It is still O(N) in the number of rows, but is very fast.
FYI I have addressed this problem in my project with this algorithm:
This causes a bit of a pause on the first frame since a very long list is actually rendered to get the heights. But then it's fast after that with accurate scrolling.
I have no problem with items with different heights.
My problem is the stick_to_end, I found that it is some kind of stick_to_begin. It is not stick to the bottom. As in the demo "Scrolling" -> "stick_to_end", when a new row is added, "stick_to_bottom"("end") should ensure the last few rows to be shown, rather than the beginning rows to be shown.
I will open a new issue for this: #2897. Any idea about this?
Or you lay out your widgets once and then store the bounds of each in a vector (i.e. the top/bottom for vertical scrolling). You can then use ScrollArea::show_viewport and do a binary search in your vector to figure out which elements need to be rendered.
I've implemented exactly this (thanks for reminding me that binary search exists :joy:) for my similar chat-like thing - and it works beautifully, and stick_to_end works great as well - 150k rows are smooth as butter, only take a few seconds for the initial render :upside_down_face:
For anyone who also stumbles upon this issue in their search (hope some form of this gets merged in the lib or extras by someone)
The only real downside is the re-/initial calculation that does a full render, which has to happen whenever the container width changes (speaking of that, would love to have windows to have only a vertical resize).
Another thing which would help with those (that I'll maybe try to do later) is chunked load - where it would actually render and cache heights of only like 1k rows around the current scroll bounds and the rest of the virtual area would be approximated by total_rest_rows * some_average_height
- this is a compromise that would have a (hopefully barely noticeable if at all) "incorrect" scrollbar that would jump a little.
I made egui_virtual_list, which supports items/rows with varying heights, and even custom layouts like here:
It works by lazily measuring the row's heights as you scroll down, and caching the heights it's already measured. If you scroll directly to the bottom of the list it will measure 1000 item heights per frame until it knows all heights, so if you have 100000+ items it will take a moment but at least it's not blocking.
My main usecase why I created this crate was to have performant infinite scroll that doesn't get slow if you keep scrolling, so the current version was made with that in mind. But I think it should be possible to create a even smarter algorithm that doesn't need to calculate all heights when you jump somewhere in the list.
For the infinite scroll usecase I also made egui_infinite_scroll as a wrapper of egui_virtual_list. You can try it in the gallery and chat example in the hello_egui demo app
I am trying to render 10.000+ items in a scroll view, which all have different heights, including multiline text and images. Currently the examples only show how to render rows with the same height when only rendering what is visible. But in lrder for me to know the height of the elements I have to lay them out first. Any pointers or thoughts on hoe to add support for this?
Thank you