You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

解决Iced中右键菜单Overlay仅显示单个按钮的问题

右键点击Widget弹出Overlay菜单仅显示单个按钮的问题

问题现象

我正在实现一个支持右键点击的Widget,点击后弹出包含多个按钮的菜单。参考iced_aw仓库示例实现后,右键点击时能在鼠标位置弹出Overlay菜单,但仅显示传入的2个按钮中的1个。

尝试调整Overlay的layout和draw方法后仍未解决。改用Element<'_, Message>类型传入按钮时,在layout阶段执行self.content.as_widget().layout(self.tree, renderer, &limits)触发数组越界错误:

index out of bounds: the len is 0 but the index is 0

具体是底层按钮尝试访问&mut tree.children[0]时出错。

两种GUI版本,一种右键点击后仅显示单个按钮,另一种显示两个按钮

相关代码片段

底层Widget的children方法

fn children(&self) -> Vec<Tree> {
    vec![Tree::new(&self.rowdata),  // 底层内容(本问题中不重要)
         Tree::new(&self.overlay_state) // 菜单内容
]
}

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,
{
    // 底层Widget状态
    state: &'a crate::row::Fake,
    button_1: Button<'a, Message, Theme, Renderer>,
    button_2: Button<'a, Message, Theme, Renderer>,
    // 更多按钮
    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,
                // 让按钮占据空间,增大这个值只会让按钮变大,不会增加空间
                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),
            ],
        );
        // 将菜单移动到预期的光标位置
        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(),
        );
        // 无论只绘制delete_button还是两个都绘制,最终只会显示"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))
    }
}

创建Overlay的代码(底层文件)

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();
    // TODO 位置需要相对于滚动区域,否则滚动后点击第6行,菜单会显示在第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(),
    )
}

(注:Iced框架存在较多样板代码,以上仅保留关键部分)

问题分析与解决方案

1. 按钮重叠显示的原因及修复

在draw方法中,两次调用layout.children().next().unwrap()都会拿到第一个子节点,导致两个按钮绘制在同一个位置,后绘制的按钮覆盖了前一个。需要按顺序获取不同的子节点,同时为每个按钮分配对应的Tree子节点:

fn draw(
    &self,
    renderer: &mut Renderer,
    theme: &Theme,
    style: &renderer::Style,
    layout: layout::Layout<'_>,
    cursor: mouse::Cursor,
) {
    let mut children = layout.children();
    
    // 绘制delete按钮,使用第一个Tree子节点
    if let Some(lay_1) = children.next() {
        self.delete_button.as_widget().draw(
            &self.tree.children[0],
            renderer,
            theme,
            style,
            lay_1,
            cursor,
            &layout.bounds(),
        );
    }
    
    // 绘制swap按钮,使用第二个Tree子节点
    if let Some(lay_2) = children.next() {
        self.quick_swap_button.as_widget().draw(
            &self.tree.children[1],
            renderer,
            theme,
            style,
            lay_2,
            cursor,
            &layout.bounds(),
        );
    }
}

2. 数组越界错误的原因及修复

当前OverlayButtons使用的tree来自底层Widget的tree.children[1],但该Tree未预先为两个按钮创建子节点。更简单的方式是使用Iced内置的Column组件管理按钮布局,自动处理Tree和Layout逻辑:

// 重构OverlayButtons结构
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 menu: Element<'a, Message, Theme, Renderer>,
    pub position: Point,
}

// 创建Overlay时用Column构建菜单
Some(
    OverlayButtons {
        tree: &mut tree.children[1],
        state: row_state,
        menu: column![
            button(text("delete")),
            button(text("Swap!"))
        ].into(),
        position: self.cursor_pos,
    }
    .into(),
)

// 简化layout和draw方法
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::ZERO, bounds);
        let node = self.menu.as_widget().layout(self.tree, renderer, &limits);
        node.move_to(self.position)
    }
    fn draw(
        &self,
        renderer: &mut Renderer,
        theme: &Theme,
        style: &renderer::Style,
        layout: layout::Layout<'_>,
        cursor: mouse::Cursor,
    ) {
        self.menu.as_widget().draw(
            self.tree,
            renderer,
            theme,
            style,
            layout,
            cursor,
            &layout.bounds(),
        );
    }
}

3. 滚动区域位置偏移修正

要解决滚动后菜单位置错误的问题,只需将光标位置加上滚动区域的偏移量:

// 在overlay方法中修正位置
let corrected_position = self.cursor_pos + translation;

内容的提问来源于stack exchange,提问作者NedNoodleHead

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 10:14:57