add iced_nav_scrollable widget crate

[?]
Jun 27, 2025, 6:19 PM
WW36JYLR4AILV7RHQEDJWMX74P74B7G7DRBHH3O2V5TCHRTZJWZQC

Dependencies

Change contents

  • file addition: iced_nav_scrollable (d--r------)
    [2.2]
  • file addition: src (d--r------)
    [0.45]
  • file addition: lib.rs (----------)
    [0.78]
    //! 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,
    })
    }
  • file addition: Cargo.toml (----------)
    [0.45]
    [package]
    name = "iced-nav-scrollable"
    description = "Iced vertically scrollable widget that allows navigation to positions of its contents"
    version.workspace = true
    edition.workspace = true
    license.workspace = true
    authors.workspace = true
    [dependencies]
    # External dependencies
    [dependencies.iced]
    workspace = true
    [dependencies.indexmap]
    workspace = true
  • edit in Cargo.toml at line 6
    [4.561]
    [2.3861]
    "iced_nav_scrollable",
  • edit in Cargo.toml at line 40
    [6.96]
    [5.1003]
    # path = "../iced"
  • edit in Cargo.toml at line 42
    [5.1060][6.97:224]()
    [workspace.dependencies.iced_runtime]
    git = "https://github.com/iced-rs/iced"
    rev = "c952ea8485b00e58bdff153989f708553272e131"
  • edit in Cargo.toml at line 45
    [6.274]
    [2.4347]
    # path = "../iced/test"
  • edit in Cargo.lock at line 2095
    [3.7479]
    [2.39486]
    ]
    [[package]]
    name = "iced-nav-scrollable"
    version = "0.1.0"
    dependencies = [
    "iced",
    "indexmap",
  • edit in Cargo.lock at line 4520
    [2.69435][2.69435:69448](),[2.69448][6.2170:2253]()
    [[package]]
    name = "scrollable"
    version = "0.1.0"
    dependencies = [
    "iced",
    "iced_runtime",
    ]