Search code examples
user-interfacerustrust-iced

How can I have a widget::Overlay contain multiple elements in iced?


So I am trying to build a widget that I can right-click, and it opens a little menu with some buttons to click. I've been following similar examples and concepts in the iced_aw repo, and I have crafted my own little solution, and I am able to make an overlay menu appear on my mouse when I right click, only problem is that it only shows 1/2 buttons I pass in.

I've tried messing around with the fn layout and the fn draw on the 'overlayed' widget, but I think I don't fully understand what is going on.

And I thought "oh maybe I should just pass in an Element<'_, Message> instead of the button", but after some troubleshooting, it seemed unable to find a child of the tree when I did it that way. It would fail with index out of bounds: the len is 0 but the index is 0 when it it got to the self.content.as_widget().layout(self.tree, renderer, &limits) part of fn layout, more specifically failing when the underlying button tried to index: &mut tree.children[0]. (assume self.content is an Element<> type.)

two versions of my gui, one that has a single button when underlying widget is rightclicked, and one menu that has 2 buttons

Though the underlying widget's children method looks normal:

    fn children(&self) -> Vec<Tree> {
        vec![Tree::new(&self.rowdata),  // the underlying content (not really important for this question)
             Tree::new(&self.overlay_state) // the menu content
]
    }

Code for the overlay:

use crate::row::{Fake, Status, Style};
use iced::advanced::layout::Limits;
use iced::advanced::{layout, renderer, widget::Tree, widget::Widget, Overlay};
use iced::advanced::{mouse, overlay};
use iced::widget::{button, column, row, text, Button, Row};
use iced::{Border, Color, Element, Length, Point, Shadow, Size, Theme, Vector};

use crate::row::Catalog;

pub struct RowMenuOverlay<'a, 'b, Message, Theme, Renderer>
where
    Message: Clone,
    Theme: Catalog + button::Catalog,
    Renderer: iced::advanced::Renderer + iced::advanced::text::Renderer,
{
    // state of underlying widget
    state: &'a crate::row::Fake,
    button_1: Button<'a, Message, Theme, Renderer>,
    button_2: Button<'a, Message, Theme, Renderer>,
    // more buttons here
    position: Point,
    tree: &'b mut Tree,
}

pub struct OverlayButtons<'a, Message, Theme, Renderer>
where
    Message: 'a + Clone,
    Theme: Catalog + button::Catalog,
    Renderer: iced::advanced::Renderer + iced::advanced::text::Renderer,
{
    pub tree: &'a mut Tree,
    pub state: &'a mut Fake,
    pub delete_button: Element<'a, Message, Theme, Renderer>,
    pub quick_swap_button: Element<'a, Message, Theme, Renderer>,
    pub position: Point,
}

impl<'a, Message, Theme, Renderer> Overlay<Message, Theme, Renderer>
    for OverlayButtons<'a, Message, Theme, Renderer>
where
    Message: Clone,
    Theme: Catalog + button::Catalog + iced::widget::text::Catalog,
    Renderer: iced::advanced::Renderer + iced::advanced::text::Renderer,
{
    fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
        let limits = Limits::new(
            Size {
                width: 100.0,
                height: 135.0,
            },
            Size {
                width: 100.0,
                height: 135.0,
            },
        );
        let node = layout::Node::with_children(
            Size {
                width: 100.0,
                // makes the button take up the space
                // adding more to this number just makes the button larger, not more space for it...
                height: 140.0,
            },
            vec![
                self.delete_button
                    .as_widget()
                    .layout(self.tree, renderer, &limits),
                self.quick_swap_button
                    .as_widget()
                    .layout(self.tree, renderer, &limits),
            ],
        );
        // puts the menu where the cursor *SHOULD* be
        node.move_to(self.position)
    }
    fn draw(
        &self,
        renderer: &mut Renderer,
        theme: &Theme,
        style: &renderer::Style,
        layout: layout::Layout<'_>,
        cursor: mouse::Cursor,
    ) {
        let lay_1 = layout.children().next().unwrap();
        self.delete_button.as_widget().draw(
            self.tree,
            renderer,
            theme,
            style,
            lay_1,
            cursor,
            &layout.bounds(),
        );
        // will only show the "quick swap button", if one of these two buttons are being drawn
        // regardless if it is ONLY the `self.delete_button`, it will still only show the "swap button"
        let lay_2 = layout.children().next().unwrap();
        self.quick_swap_button.as_widget().draw(
            self.tree,
            renderer,
            theme,
            style,
            lay_2,
            cursor,
            &layout.bounds(),
        );
    }
}

impl<'a, Message, Theme, Renderer> From<OverlayButtons<'a, Message, Theme, Renderer>>
    for iced::advanced::overlay::Element<'a, Message, Theme, Renderer>
where
    Message: 'a + Clone,
    Theme: 'a + Catalog + button::Catalog + iced::widget::text::Catalog,
    Renderer: 'a + iced::advanced::Renderer + iced::advanced::text::Renderer,
{
    fn from(overlay: OverlayButtons<'a, Message, Theme, Renderer>) -> Self {
        Self::new(Box::new(overlay))
    }
}

When I create the overlay in the underlying file:

    fn overlay<'b>(
        &'b mut self,
        tree: &'b mut Tree,
        layout: layout::Layout<'_>,
        renderer: &Renderer,
        translation: Vector,
    ) -> Option<iced::advanced::overlay::Element<'b, Message, Theme, Renderer>> {
        let bounds = layout.bounds();
        let position = Point::new(bounds.center_x(), bounds.center_y());
        if !self.show_menu {
            return None;
        }
        let row_state: &mut Fake = tree.state.downcast_mut();
        // let position = Point::new(bounds.center_x(), bounds.center_y());
        // let position = Point::new(360.0, 430.0);
        // TODO the position ^^ needs to be relative to the scrollable, because if you scroll 2x rows, click on #6, it will show
        // the menu on #8
        Some(
            OverlayButtons {
                tree: &mut tree.children[1],
                state: row_state,
                delete_button: button(text("delete")).into(),
                quick_swap_button: button(text("Swap!")).into(),
                position: self.cursor_pos,
            }
            .into(),
        )
    }

(sorry if this is a lot of code, iced has some boilerplate, I tried to only include the important stuff)


Solution

  • So I got this to work. The first problem I had was just a matter of passing in the correct state into certain function. I don't think it was included in the original snippets I posted, but I was passing in self.tree.children[0] when it was supposed to be self.tree.

    And the other problem I solved through that was indeed passing in the Element<'_, Message, Theme, Renderer>. But I had the problem of is I passed it into the underlying widget, I could not give it to the overlay widget to use. It would complain that the data is owned by the underlying widget, and it doesn't implement clone or copy. And you are able to just pass in a reference to it, but you are not able to use on_event for the overlay, since that requires a mutable instance of the widget.

    So the solution? Making a function that creates the overlay:

    pub fn create_menu<'a, Message, Theme, Renderer>(
        delete_msg: Message,
    ) -> Element<'a, Message, Theme, Renderer>
    where
        Message: 'a + Clone,
        Theme: 'a + Catalog + button::Catalog + iced::widget::text::Catalog,
        Renderer: 'a + iced::advanced::Renderer + iced::advanced::text::Renderer,
    {
        column![
            button(text("delete")).on_press(delete_msg),
            button(text("Edit")),
            button(text("Quickswap 1")),
            button(text("add to.."))
        ]
        .into()
    }
    

    And that function is called in a few places in the code:

    as the Element<> type in the fn new overlay struct:

    pub fn new(...
        content: create_menu(delete_msg)
        ...
    

    In the children function in the underlying widget:

    fn children(&self) -> Vec<Tree> {
         Tree::new(&self.row_data) // <-- also `Element<>`
         Tree:new(&create_menu::<Message, Theme, Renderer>(self.delete_msg.clone()))
        // im not sure why it wanted type params...
    

    and finally inside fn overlay inside the underlying widget's impl

    fn overlay ...
    Some ( OverlayButtons { // <--- the type that we are returning
    ...
    content: create_menu(self.delete_msg.clone()),
    

    There were also some other hiccups I experience while making this idea work, such as opening the menu inside the scrollable would not put it in the right spot if I was scrolled down a bit, and some layout issues. But I won't get into detail about those, it's outside the scope of the question, but the code will be reflected on my github project: punge