//! Vertically scrollable widget that allows scrolling to the positions of
//! its contents.
use iced::widget::{self, container, scrollable};
use iced::{advanced, window, Element, Rectangle, Subscription, Task};
use indexmap::IndexMap;
#[derive(Debug)]
pub struct NavScrollable {
pub offsets_ready: bool,
pub offsets: Vec<f32>,
pub y_offset: f32,
pub id: scrollable::Id,
pub sections: IndexMap<container::Id, f32>,
}
#[derive(Debug, Clone)]
pub enum Msg {
RefreshOffsets,
GotHeight { id: container::Id, height: f32 },
Scrolled(scrollable::Viewport),
ScrollToPrev,
ScrollToNext,
}
pub fn init(contents_count: usize) -> (NavScrollable, Task<Msg>) {
let (sections, tasks): (IndexMap<_, _>, Vec<_>) = (0..contents_count)
.map(|_i| {
let id = container::Id::unique();
let section = (id.clone(), 0.0_f32);
let id_clone = id.clone();
let task =
height_task(id.clone()).map(move |height| Msg::GotHeight {
id: id_clone.clone(),
height,
});
(section, task)
})
.collect();
let id = scrollable::Id::unique();
(
NavScrollable {
offsets_ready: false,
// To be computed from sizes once tasks complete
offsets: vec![0.0; sections.len()],
y_offset: 0.0,
id,
sections,
},
Task::batch(tasks),
)
}
pub fn subs() -> Subscription<Msg> {
window::resize_events().map(|_| Msg::RefreshOffsets)
}
pub fn update(nav: &mut NavScrollable, msg: Msg) -> Task<Msg> {
match msg {
Msg::RefreshOffsets => {
reset_offsets(nav);
Task::batch(nav.sections.keys().map(|id| {
let id_clone = id.clone();
height_task(id.clone()).map(move |height| Msg::GotHeight {
id: id_clone.clone(),
height,
})
}))
}
Msg::GotHeight { id, height } => {
reset_offsets(nav);
nav.sections.insert(id, height);
Task::none()
}
Msg::Scrolled(viewport) => {
nav.y_offset = viewport.absolute_offset().y;
Task::none()
}
Msg::ScrollToNext => {
update_offsets(nav);
if nav.offsets_ready {
if let Some(y) =
nav.offsets.iter().find(|offset| *offset > &nav.y_offset)
{
return scrollable::scroll_to(
nav.id.clone(),
scrollable::AbsoluteOffset { x: 0.0, y: *y },
);
}
}
Task::none()
}
Msg::ScrollToPrev => {
update_offsets(nav);
if nav.offsets_ready {
if let Some(y) = nav
.offsets
.iter()
.rev()
.find(|offset| *offset < &nav.y_offset)
{
return scrollable::scroll_to(
nav.id.clone(),
scrollable::AbsoluteOffset { x: 0.0, y: *y },
);
}
}
Task::none()
}
}
}
/// NOTE: call `into_iter()` on the children parameter to make it type-check
/// with `ExactSizeIterator`.
pub fn view<'a, Message, Theme, Renderer, F>(
nav: &NavScrollable,
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>
+ ExactSizeIterator,
map_msg: F,
) -> Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: container::Catalog + scrollable::Catalog + 'a,
Renderer: iced::advanced::Renderer + 'a,
F: Fn(Msg) -> Message + 'a,
{
debug_assert_eq!(nav.sections.len(), children.len(), "The `NavScrollable` was most likely initialized with a count different from the number of actual children given to the the view function. Count is {}, but got {} children", nav.sections.len(), children.len());
let children = children
.into_iter()
.zip(nav.sections.keys())
.map(|(child, id)| Element::from(container(child).id(id.clone())));
Element::from(
widget::scrollable(widget::column(children))
.id(nav.id.clone())
.on_scroll(move |viewport| map_msg(Msg::Scrolled(viewport))),
)
}
fn reset_offsets(nav: &mut NavScrollable) {
nav.offsets_ready = false;
}
fn update_offsets(nav: &mut NavScrollable) {
if !nav.offsets_ready {
debug_assert_eq!(nav.sections.len(), nav.offsets.len());
let mut acc = 0.0_f32;
nav.sections.values().enumerate().for_each(|(i, height)| {
// The current offset is the value of the acc
nav.offsets[i] = acc;
// Increment the acc with the height of this section
acc += *height;
});
nav.offsets_ready = true;
}
}
/// Produces a [`Task`] that queries the height of the [`Container`] with the
/// given [`Id`].
fn height_task(id: impl Into<container::Id>) -> Task<f32> {
let id = id.into();
struct SizeOp {
target: advanced::widget::Id,
height: Option<f32>,
}
impl advanced::widget::Operation<f32> for SizeOp {
fn container(
&mut self,
id: Option<&advanced::widget::Id>,
bounds: Rectangle,
operate_on_children: &mut dyn FnMut(
&mut dyn advanced::widget::Operation<f32>,
),
) {
if id == Some(&self.target) {
self.height = Some(bounds.height);
return;
}
operate_on_children(self);
}
fn finish(&self) -> advanced::widget::operation::Outcome<f32> {
advanced::widget::operation::Outcome::Some(
self.height.expect("Must be able to determine the size. If this even panics, make the height optional and re-try the task when None"),
)
}
}
advanced::widget::operate(SizeOp {
target: id.into(),
height: None,
})
}