lucasmerlin / hello_egui

A collection of useful crates for egui
https://lucasmerlin.github.io/hello_egui/
MIT License
272 stars 24 forks source link

Help with combining dnd with virtual list #29

Closed tpstevens closed 1 month ago

tpstevens commented 1 month ago

Hello! I'm trying to combine the virtual list with a drag-and-drop list, and I'm seeing some strange behavior with rendering and dragging. I think it comes down to the way that I'm calculating the number of items to render within dnd::show(), but I'm not sure if I'm approaching the problem correctly.

I've started with the virtual list example and refactored out the drawing to be able to draw dnd elements. Here's the overall structure:

pub fn main() -> eframe::Result<()> {
    let mut items: Vec<_> = (0..100).collect();
    let mut virtual_list = VirtualList::new();
    let mut dragged_item_idx: Option<usize> = None; // used for dragging and dropping across 

    eframe::run_simple_native(
        "Virtual List Drag-and-Drop Example",
        Default::default(),
        move |ctx, _frame| {
            CentralPanel::default().show(ctx, |ui| {
                ScrollArea::vertical().show(ui, |ui| {
                    ui.set_width(ui.available_width());
                    // Wrap dnd inside virtual list (see next code snippet)
                });
            });
        },
    )
}

// Helper function for drawing draggable elements
fn draw_item(ui: &mut egui::Ui, handle: egui_dnd::Handle, item: &i32) -> egui::Rect {
    let mut rng = StdRng::seed_from_u64(*item as u64);
    let height = rng.gen_range(0.0..=100.0);

    Frame::canvas(ui.style())
        .inner_margin(Margin::symmetric(16.0, 8.0 + height / 2.0))
        .show(ui, |ui| {
            ui.set_width(ui.available_width());
            handle.ui(ui, |ui| {
                ui.label(format!("Item {}", item));
            });
        }).response.rect
}

And here's how I'm wrapping the dnd list inside the virtual list:

virtual_list.ui_custom_layout(ui, items.len(), |ui, start_index| {
    let mut num_shown = 0;
    let mut used_height = 0f32;

    let response = egui_dnd::dnd(ui, "dnd").show(
        items[start_index..].iter_mut(),
        |ui, item, handle, _dragging| {
            // TODO: exit early if used height exceeds available height
            used_height += draw_item(ui, handle, item).height()
                + ui.spacing().item_spacing.y;
            num_shown += 1;
        },
    );

    // Handle drag start and drag stop
    match dragged_item_idx {
        None => {
            if let Some(update) = &response.update {
                dragged_item_idx = Some(start_index + update.from);
            }
        }
        Some(idx) => {
            if let Some(update) = &response.final_update() {
                // Convert indices in slice to indices in original vec
                egui_dnd::utils::shift_vec(
                    idx,
                    start_index + update.to,
                    &mut items,
                );

                dragged_item_idx = None;
            }
        }
    }

    num_shown
});

As written above, this code draws every single element each time, as the layout closure passed to virtual_list::ui_custom_layout draws every single item. I've tried a number of different methods to calculate how many elements I should render based on the available space, but I haven't been able to account for the scroll area's current offset and the virtual list's overscan. It's pretty easy to get the maximum vertical area (as that can be calculated with the central panel's ui.available_height() or cached from the result of ScrollArea::vertical().show()), but it's not clear to me how to take into account the scroll area's current offset.

Ignoring that issue and rendering a fixed number of items, I haven't been able to drag an item cleanly across dnd "boundaries". For example, if I render 10 item in dnd::show(), the virtual list will sometimes call the layout closure with a start_index of 0 and a start_index of 10 (which makes sense). If I drag an item within indices 0 to 9 or 10 to 19, the other items appear to move out of the way as the item is dragged through them and the list is properly updated upon release. If I drag an item from index 2 to index 14, it behaves properly until it passes index 9, and then it appears to float over the next elements instead of them moving out of the way. I'm not sure how to handle this, because from the perspective of each call to dnd::show(), the iterator at the start of the drag is different than the iterator at the end (if the layout closure passed to virtual_list::show() is called multiple times).

In summary, my questions are:

Thanks very much! Once I get this sorted out, I'm happy to clean it up and submit it as a PR for a virtual list + dnd example.

lucasmerlin commented 1 month ago

Hi! Have you looked at the infinite_scroll dnd example? It should be pretty similar to the virtual_list, since internally infinite_scroll is just a wrapper around a virtual_list? If that doesn't help I can try to create a virtual_list example tomorrow.

tpstevens commented 1 month ago

Aha -- I missed that because I only searched for virtual list. I see what I'm doing wrong now. Thanks very much!