// From iced_aw, license MIT //! A widget that handles menu trees use std::collections::HashMap; use super::{ menu_inner::{ CloseCondition, Direction, ItemHeight, ItemWidth, Menu, MenuState, PathHighlight, }, menu_tree::MenuTree, }; use crate::{ Renderer, style::menu_bar::StyleSheet, widget::{ RcWrapper, dropdown::menu::{self, State}, }, }; use iced::{Point, Vector, window}; use iced_core::Border; use iced_widget::core::{ Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget, event, layout::{Limits, Node}, mouse::{self, Cursor}, overlay, renderer::{self, Renderer as IcedRenderer}, touch, widget::{Tree, tree}, }; /// A `MenuBar` collects `MenuTree`s and handles all the layout, event processing, and drawing. pub fn menu_bar(menu_roots: Vec>) -> MenuBar where Message: Clone + 'static, { MenuBar::new(menu_roots) } #[derive(Clone, Default)] pub(crate) struct MenuBarState { pub(crate) inner: RcWrapper, } pub(crate) struct MenuBarStateInner { pub(crate) tree: Tree, pub(crate) popup_id: HashMap, pub(crate) pressed: bool, pub(crate) bar_pressed: bool, pub(crate) view_cursor: Cursor, pub(crate) open: bool, pub(crate) active_root: Vec>, pub(crate) horizontal_direction: Direction, pub(crate) vertical_direction: Direction, pub(crate) menu_states: Vec>, } impl MenuBarStateInner { pub(super) fn get_trimmed_indices(&self, index: usize) -> impl Iterator + '_ { self.menu_states .get(index) .into_iter() .map(|v| v.iter()) .flatten() .take_while(|ms| ms.index.is_some()) .map(|ms| ms.index.expect("No indices were found in the menu state.")) } pub(super) fn reset(&mut self) { self.open = false; self.active_root = Vec::new(); self.menu_states.clear(); } } impl Default for MenuBarStateInner { fn default() -> Self { Self { tree: Tree::empty(), pressed: false, view_cursor: Cursor::Available([-0.5, -0.5].into()), open: false, active_root: Vec::new(), horizontal_direction: Direction::Positive, vertical_direction: Direction::Positive, menu_states: Vec::new(), popup_id: HashMap::new(), bar_pressed: false, } } } pub(crate) fn menu_roots_children(menu_roots: &Vec>) -> Vec where Message: Clone + 'static, { /* menu bar menu root 1 (stateless) flat tree menu root 2 (stateless) flat tree ... */ menu_roots .iter() .map(|root| { let mut tree = Tree::empty(); let flat = root .flattern() .iter() .map(|mt| Tree::new(mt.item.clone())) .collect(); tree.children = flat; tree }) .collect() } #[allow(invalid_reference_casting)] pub(crate) fn menu_roots_diff(menu_roots: &mut Vec>, tree: &mut Tree) where Message: Clone + 'static, { if tree.children.len() > menu_roots.len() { tree.children.truncate(menu_roots.len()); } tree.children .iter_mut() .zip(menu_roots.iter()) .for_each(|(t, root)| { let mut flat = root .flattern() .iter() .map(|mt| { let widget = &mt.item; let widget_ptr = widget as *const dyn Widget; let widget_ptr_mut = widget_ptr as *mut dyn Widget; //TODO: find a way to diff_children without unsafe code unsafe { &mut *widget_ptr_mut } }) .collect::>(); t.diff_children(flat.as_mut_slice()); }); if tree.children.len() < menu_roots.len() { let extended = menu_roots[tree.children.len()..].iter().map(|root| { let mut tree = Tree::empty(); let flat = root .flattern() .iter() .map(|mt| Tree::new(mt.item.clone())) .collect(); tree.children = flat; tree }); tree.children.extend(extended); } } pub fn get_mut_or_default(vec: &mut Vec, index: usize) -> &mut T { if index < vec.len() { &mut vec[index] } else { vec.resize_with(index + 1, T::default); &mut vec[index] } } /// A `MenuBar` collects `MenuTree`s and handles all the layout, event processing, and drawing. #[allow(missing_debug_implementations)] pub struct MenuBar { width: Length, height: Length, spacing: f32, padding: Padding, bounds_expand: u16, main_offset: i32, cross_offset: i32, close_condition: CloseCondition, item_width: ItemWidth, item_height: ItemHeight, path_highlight: Option, menu_roots: Vec>, style: ::Style, window_id: window::Id, #[cfg(feature = "wayland")] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, pub(crate) on_surface_action: Option Message>>, } impl MenuBar where Message: Clone + 'static, { /// Creates a new [`MenuBar`] with the given menu roots #[must_use] pub fn new(menu_roots: Vec>) -> Self { let mut menu_roots = menu_roots; menu_roots.iter_mut().for_each(MenuTree::set_index); // println!("======================================================"); // for menu_root in &menu_roots { // dbg!(menu_root.index); // for inner_root in &menu_root.children { // dbg!(inner_root.index); // } // } Self { width: Length::Shrink, height: Length::Shrink, spacing: 0.0, padding: Padding::ZERO, bounds_expand: 16, main_offset: 0, cross_offset: 0, close_condition: CloseCondition { leave: false, click_outside: true, click_inside: true, }, item_width: ItemWidth::Uniform(150), item_height: ItemHeight::Uniform(30), path_highlight: Some(PathHighlight::MenuActive), menu_roots, style: ::Style::default(), window_id: window::Id::NONE, #[cfg(feature = "wayland")] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), on_surface_action: None, } } /// Sets the expand value for each menu's check bounds /// /// When the cursor goes outside of a menu's check bounds, /// the menu will be closed automatically, this value expands /// the check bounds #[must_use] pub fn bounds_expand(mut self, value: u16) -> Self { self.bounds_expand = value; self } /// [`CloseCondition`] #[must_use] pub fn close_condition(mut self, close_condition: CloseCondition) -> Self { self.close_condition = close_condition; self } /// Moves each menu in the horizontal open direction #[must_use] pub fn cross_offset(mut self, value: i32) -> Self { self.cross_offset = value; self } /// Sets the height of the [`MenuBar`] #[must_use] pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// [`ItemHeight`] #[must_use] pub fn item_height(mut self, item_height: ItemHeight) -> Self { self.item_height = item_height; self } /// [`ItemWidth`] #[must_use] pub fn item_width(mut self, item_width: ItemWidth) -> Self { self.item_width = item_width; self } /// Moves all the menus in the vertical open direction #[must_use] pub fn main_offset(mut self, value: i32) -> Self { self.main_offset = value; self } /// Sets the [`Padding`] of the [`MenuBar`] #[must_use] pub fn padding>(mut self, padding: P) -> Self { self.padding = padding.into(); self } /// Sets the method for drawing path highlight #[must_use] pub fn path_highlight(mut self, path_highlight: Option) -> Self { self.path_highlight = path_highlight; self } /// Sets the spacing between menu roots #[must_use] pub fn spacing(mut self, units: f32) -> Self { self.spacing = units; self } /// Sets the style of the menu bar and its menus #[must_use] pub fn style(mut self, style: impl Into<::Style>) -> Self { self.style = style.into(); self } /// Sets the width of the [`MenuBar`] #[must_use] pub fn width(mut self, width: Length) -> Self { self.width = width; self } #[cfg(feature = "wayland")] pub fn with_positioner( mut self, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, ) -> Self { self.positioner = positioner; self } pub fn window_id(mut self, id: window::Id) -> Self { self.window_id = id; self } pub fn window_id_maybe(mut self, id: Option) -> Self { if let Some(id) = id { self.window_id = id; } self } pub fn on_surface_action( mut self, handler: impl Fn(crate::surface::Action) -> Message + 'static, ) -> Self { self.on_surface_action = Some(Box::new(handler)); self } } impl Widget for MenuBar where Message: Clone + 'static, { fn size(&self) -> iced_core::Size { iced_core::Size::new(self.width, self.height) } fn diff(&mut self, tree: &mut Tree) { let state = tree.state.downcast_mut::(); state .inner .with_data_mut(|inner| menu_roots_diff(&mut self.menu_roots, &mut inner.tree)) } fn tag(&self) -> tree::Tag { tree::Tag::of::() } fn state(&self) -> tree::State { tree::State::new(MenuBarState::default()) } fn children(&self) -> Vec { menu_roots_children(&self.menu_roots) } fn layout(&self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node { use super::flex; let limits = limits.width(self.width).height(self.height); let children = self .menu_roots .iter() .map(|root| &root.item) .collect::>(); // the first children of the tree are the menu roots items let mut tree_children = tree .children .iter_mut() .map(|t| &mut t.children[0]) .collect::>(); flex::resolve_wrapper( &flex::Axis::Horizontal, renderer, &limits, self.padding, self.spacing, Alignment::Center, &children, &mut tree_children, ) } fn on_event( &mut self, tree: &mut Tree, event: event::Event, layout: Layout<'_>, view_cursor: Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) -> event::Status { use event::Event::{Mouse, Touch}; use mouse::{Button::Left, Event::ButtonReleased}; use touch::Event::{FingerLifted, FingerLost}; let root_status = process_root_events( &mut self.menu_roots, view_cursor, tree, &event, layout, renderer, clipboard, shell, viewport, ); let my_state = tree.state.downcast_mut::(); // XXX this should reset the state if there are no other copies of the state, which implies no dropdown menus open. let reset = self.window_id != window::Id::NONE && my_state.inner.with_data(|d| d.open); my_state.inner.with_data_mut(|state| { if reset { if let Some(popup_id) = state.popup_id.get(&self.window_id).copied() { dbg!("reset destroy"); // TODO emit message if let Some(handler) = self.on_surface_action.as_ref() { shell.publish((handler)(crate::surface::Action::DestroyPopup(popup_id))); state.reset(); } } } let tree = &mut state.tree; match event { Mouse(ButtonReleased(Left)) | Touch(FingerLifted { .. } | FingerLost { .. }) => { if state.menu_states.is_empty() && view_cursor.is_over(layout.bounds()) { state.view_cursor = view_cursor; state.open = true; // #[cfg(feature = "wayland")] // TODO emit Message to open menu } } _ => (), } }); root_status } fn draw( &self, tree: &Tree, renderer: &mut Renderer, theme: &crate::Theme, style: &renderer::Style, layout: Layout<'_>, view_cursor: Cursor, viewport: &Rectangle, ) { let state = tree.state.downcast_ref::(); let cursor_pos = view_cursor.position().unwrap_or_default(); state.inner.with_data_mut(|state| { let position = if state.open && (cursor_pos.x < 0.0 || cursor_pos.y < 0.0) { state.view_cursor } else { view_cursor }; // draw path highlight if self.path_highlight.is_some() { let styling = theme.appearance(&self.style); if let Some(active) = get_mut_or_default(&mut state.active_root, 0).get(0) { let active_bounds = layout .children() .nth(*active) .expect("Active child not found in menu?") .bounds(); let path_quad = renderer::Quad { bounds: active_bounds, border: Border { radius: styling.bar_border_radius.into(), ..Default::default() }, shadow: Default::default(), }; renderer.fill_quad(path_quad, styling.path); } } self.menu_roots .iter() .zip(&tree.children) .zip(layout.children()) .for_each(|((root, t), lo)| { root.item.draw( &t.children[root.index], renderer, theme, style, lo, position, viewport, ); }); }); } fn overlay<'b>( &'b mut self, tree: &'b mut Tree, layout: Layout<'_>, _renderer: &Renderer, translation: Vector, ) -> Option> { //#[cfg(feature = "wayland")] //return None; let state = tree.state.downcast_ref::(); if state .inner .with_data_mut(|state| !state.open || state.active_root.is_empty()) { return None; }; Some( Menu { tree: state.clone(), menu_roots: std::borrow::Cow::Owned(self.menu_roots.clone()), bounds_expand: self.bounds_expand, menu_overlays_parent: false, close_condition: self.close_condition, item_width: self.item_width, item_height: self.item_height, bar_bounds: layout.bounds(), main_offset: self.main_offset, cross_offset: self.cross_offset, root_bounds_list: layout.children().map(|lo| lo.bounds()).collect(), path_highlight: self.path_highlight, style: std::borrow::Cow::Borrowed(&self.style), position: Point::new(translation.x, translation.y), is_overlay: false, window_id: window::Id::NONE, depth: 0, } .overlay(), ) } } impl<'a, Message> From> for Element<'a, Message, crate::Theme, Renderer> where Message: Clone + 'static, { fn from(value: MenuBar) -> Self { Self::new(value) } } #[allow(unused_results, clippy::too_many_arguments)] fn process_root_events( menu_roots: &mut [MenuTree], view_cursor: Cursor, tree: &mut Tree, event: &event::Event, layout: Layout<'_>, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) -> event::Status where { menu_roots .iter_mut() .zip(&mut tree.children) .zip(layout.children()) .map(|((root, t), lo)| { // assert!(t.tag == tree::Tag::stateless()); root.item.on_event( &mut t.children[root.index], event.clone(), lo, view_cursor, renderer, clipboard, shell, viewport, ) }) .fold(event::Status::Ignored, event::Status::merge) }