elkowar/eww

[FEATURE] Add autoscroll option to scroll widget or create listbox and listrow widgets to implement with scroll widget

Opened this issue · 2 comments

Description of the requested feature

Currently the scroll widget works pretty well but only allows for manual scrolling to find the selected item. Investigating how to make ScrolledWindow scroll automatically to the selected item, I found that it can only be done using the ListBox and ListBoxRow widgets. And the idea is to add two more widgets to achieve this or if it is possible to add this feature to the scroll widget.

Example:

2024-08-12_18-33-57@1920x1080.mp4

Proposed configuration syntax

Currently the for widget can only be used inside a box widget, but it would be a good idea to change this to be able to use other types of widgets like listbox. This way

(scroll
  :hscroll false
  :vscroll true
  :height 120
  :width 350
  (listbox
    :position 15
    (for song in playlist
      (listrow
        (button
          :class "${song.id == current_song_id ? 'active' : ''}"
          :onclick "mpc play ${song.id}"
          (label
            :text "${song.name}"
          )
        )
      )
    )
  )
)

The listbox position option is to know the current position of the list and go to the current item, which internally would be handled with the listbox row_at_index(position) method

Additional context

In order to create the example in the video I had to create the listbox and listrow widgets.

const WIDGET_NAME_LISTBOX: &str = "listbox";
fn build_gtk_listbox(bargs: &mut BuilderArgs) -> Result<gtk::ListBox> {
    let gtk_widget = gtk::ListBox::new();

    let mut children = bargs.widget_use.children.iter().map(|child| {
        build_gtk_widget(
            bargs.scope_graph,
            bargs.widget_defs.clone(),
            bargs.calling_scope,
            child.clone(),
            bargs.custom_widget_invocation.clone(),
        )
    });

    let child = children.next().unwrap()?;
    gtk_widget.add(&child);

    let last_total_rows = 0;

    def_widget!(bargs, _g, gtk_widget, {
        prop(position: as_i32) {
            // II had to do this so I could get all the listrows inside the box widget,
            // since the box widget that contains the for widget is not able to realize that the content has changed or is dynamic, or so I think.
            if let Some(gbox) = child.dynamic_cast_ref::<gtk::Box>() {
                let current_total_rows = gbox.children().len();


                if current_total_rows > last_total_rows {
                    for (i, child) in gbox.children().iter().enumerate() {
                        // Remove all children from the box widget so that they can be added to the listbox widget without causing errors
                        gbox.remove(child);
                        // This is to avoid adding the first child, since it is empty.
                        if i > 0 {
                            gtk_widget.add(child);
                        }
                    }
                }
            }

            // Select ListBoxRow by position
            gtk_widget.select_row(gtk_widget.row_at_index(position).as_ref());

            // Autocroll handling
            if let Some(row) = gtk_widget.selected_row() {
                match row.translate_coordinates(&gtk_widget, 0, 0) {
                    Some((x, y)) => match gtk_widget.adjustment() {
                        Some(adjustment) => {
                            let page_size = adjustment.page_size();
                            let (_, height) = row.preferred_height();
                            let (_, width) = row.preferred_width();

                            // For horizontal scrolling
                            if x >= 0 {
                                adjustment.set_value(x as f64 - (page_size - width as f64) / 2.0);
                            }

                            // For vertical scrolling
                            if y >= 0 {
                                adjustment.set_value(y as f64 - (page_size - height as f64) / 2.0);
                            }
                        },
                        None => {}
                    }
                    None => {}
                }
            }
        }
    });

    Ok(gtk_widget)
}

const WIDGET_NAME_LISTROW: &str = "listrow";
fn build_gtk_listrow(bargs: &mut BuilderArgs) -> Result<gtk::ListBoxRow> {
    let gtk_widget = gtk::ListBoxRow::new();

    def_widget!(bargs, _g, gtk_widget, {
        prop(activatable: as_bool = true, selectable: as_bool = true) {
            gtk_widget.set_activatable(activatable);
            gtk_widget.set_selectable(selectable);
        }
    });

    Ok(gtk_widget)
}

Usage example:

(scroll
  :hscroll false
  :vscroll true
  :height 120
  :width 350
  (listbox
    :position 15
    (box
      :orientation "v"
      (for song in playlist
        (listrow
          (button
            :class "${song.id == current_song_id ? 'active' : ''}"
            :onclick "mpc play"
            (label
              :text "${song.name}"
            )
          )
        )
      )
    )
  )
)

Does this project have discord?

Would love to see this implemented as well. Would open up some cool possibilities.