解决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]时出错。

相关代码片段
底层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
相关产品推荐
相关产品推荐

