Always showing scrollbar in nested view?
First: is this a bug report? A suggestion? Or asking for help?
Asking for help.
Problem description
I've trying to create a spreadsheet-like custom View. it consists of a vertical LinearLayout with two Canvas child views: a fixed-height column header, and a variable-height records body, both of which have the same width. I'm looking to add scrolling to this view, in the following way:
- The header should scroll horizontally along with the records, similar to how the column labels horizontally scroll along with content in Excel/Calc.
- Only the records
Canvasshould scroll vertically. The idea is that the header should always be visible, even if scrolled all the way to the bottom of a long list of records. - The vertical scrollbar on the records
Canvasshould always be visible if there are enough records to need a vertical scrollbar.
As of right now, I create/configure the header Canvas, create/configure the records Canvas (including making it scrollable: .scrollable().scroll_x(false).scroll_y(true)), add them both as children to the LinearLayout, and then make the LinearLayout scrollable in the X direction only.
This is so close to working, but I can't seem to get (3) in my list above to work: if I'm scrolled all the way to the left, I can't see the vertical scrollbar. Any ideas on how to make this work?
If needed, I can provide screenshots of how things look now, as well as code. It's quite verbose, so I omitted it for now.
Environment
- Operating system: Ubuntu 20.04
- Backend used: termion
- Current locale: en_US.UTF-8
- Cursive version: 0.15
Hi, and thanks for the report!
Indeed, it doesn't seem easy to achieve that right now.
I think the easiest might be a ScrollView with just the content, and a separate view for the header (without visible scrollbar, and not reacting to user input). You'd then have to keep both in sync, by manually scrolling the header every time the content is scrolled.
I'm not sure it'd work today, as:
- We seem to be missing a
on_scrollcallback forScrollView. - I'm not 100% sure we can easily have a
ScrollViewwith user input disabled, but still scrollable through its API (likeScrollView::scroll_x). Maybe making the view non-focusable could work?
Thank you for the explanation @gyscos! Hmm, do does this mean what I'm trying to do isn't possible with the current featureset of cursive?
If so, for the time being, what I have is "close enough", but if there's a way I can help with making this be possible, I'd like to help!
It's possible, but not easy. Right now it probably would require you to write your own View using the scroll module directly.
Hopefully we'll make it easier to achieve using the builtin ScrollView.
I'll have to give that a try then, thank you for helping me in the right direction! What would be the best way to get familiar with the scroll module?
You can have a look at ScrollView itself. MenuPopup also uses it.
Basically you embed a scroll::Core in your view, and implement scroll::Scroller. Then the free functions from the scroll module let you implement View around whatever you are wrapping. You'd pretty much rewrite ScrollView, but print the header separately. You can use scroll::Core::content_viewport to know the current scroll offset (for example core.content_viewport().left()) to know what part of the header you should print.
Feel free to ask if you have any question - you can also drop by on gitter.
I also just added ScrollView::on_scroll and a few related functions. These should let you listen to any change in the viewport used, so you could potentially update other views to match.
Thinking back about using the scroll module, a cleaner solution that was already possible would be to put the header in the ScrollView as well, but in scroll::draw, always print the header on top (using the offset). Something like:
struct YourView {
core: cursive::view::scroll::Core,
}
cursive::impl_scroller!(YourView::core);
impl YourView {
fn get_header(&self) -> &str { "" }
fn print_content(&self, printer: &Printer) {}
}
impl View for YourView {
fn draw(&self, printer: &Printer) {
let offset = self.core.content_viewport();
scroll::draw(self, printer, |s, p| {
// Draw the content under the header first, so the header overwrites it.
s.print_content(&p.offset((0,1)));
// Draw the header always at the top position (but all the way to the left, so it scrolls with the content).
p.print((0, offset.top()), s.get_header());
// You could also print something on the left column for example
});
}
}
Note that here you "take" 1 row from the child, so you need to update layout and required_size to match that.
Thank you @gyscos! I've got the header staying in place and vertical scrolling of just the rows, but now I have one more issue: when I set up the rows to be very wide, and call .scrollable() on my custom view, it scrolls horizontally and vertically, but the vertical scrollbar isn't visible anymore. Any suggestions?
To have both scrollbars always visible you'll want to have a single view handle the scrolling, either a ScrollView (through the .scrollable() wrapper) or your own view. If you use both, then the nested view will not be able to keep its scrollbar always visible.
To do this, you'll just need to handle both vertical and horizontal scrolling in your custom view. You'll need to align the headers with the content; you can do that by checking core.content_viewport().left() for example.
I think I've got it working 99% of the way now using a ScrollView and core.content_viewport().left(), thank you @gyscos!
The remaining 1% is an edge case when I have an inner view that is too wide and needs a horizontal scrollbar. If N is the maximum number of rows that should be visible in the inner view without needing to render a vertical scrollbar, N - 1 and N + 1 work as expected (no vertical scrollbar + all rows visible, and vertical scrollbar present + some rows not visible until scrolling down, respectively). However, N does not work: the horizontal scrollbar seems to cover up the last record, and it's not visible, and there's no vertical scrollbar rendered!
I tried playing around with the layout and required_size, but can't seem to solve this edge case. Are there any things that I might be forgetting?
Looks like it was a bug in the scroll module - I just pushed a fix on master, tell me if it helps!
I tried it right now, using a git dependency, but I seem to have a conflicting requirement with the num-integer crate. Both cursive_core and chrono want different, non-compatible versions of it.
Weird - maybe cargp update can help?
Looks like cargo update fixed the package issue, but I'm still seeing the same edge case issue! At that exact number of rows N, I'm able to scroll down, but no vertical scrollbar appears.
Here's a short example that can replicate the issue (assuming that lorem.txt has exactly enough lines to perfectly fill the height of your terminal):
use cursive::Cursive;
use cursive::CursiveExt;
use cursive::view::Scrollable;
use cursive::views::TextView;
fn main() {
let text = include_str!("../lorem.txt");
let mut siv = Cursive::default();
let inner_view =
TextView::new(text)
.no_wrap()
.scrollable()
.scroll_x(true)
.scroll_y(true)
;
siv.add_fullscreen_layer(inner_view);
siv.run();
}
This is also with the following in Cargo.toml:
[dependencies]
cursive = { git = "https://github.com/gyscos/cursive", default-features = false, features = ["termion-backend"] }
Ah true, it was a larger issue than I thought. Just pushed another tentative fix - does it help?
Just tested it, it works! Thank you so much, I really appreciate the hands-on help and patience.