iced_aw/widget/menu/
common.rs

1use iced_core::{
2    Padding, Rectangle, Shell, Size,
3    layout::{Layout, Node},
4    mouse, renderer,
5    widget::Tree,
6};
7
8use super::menu_bar::{GlobalState, MenuBarTask};
9use super::menu_tree::{Item, MenuState};
10use crate::style::menu_bar::Catalog;
11
12///
13/// ## FakeHovering:
14///
15/// Places cursors at the path items,
16/// useful when you want to customize the styling of each item in the path,
17/// or you simple want the look of the items when they are hovered over.
18///
19/// The downside is when some widget in the path don't response to hovering,
20/// the path won't be fully drawn, and when you want uniform path styling
21/// but some widget response to hovering differently.
22///
23/// ## Backdrop:
24///
25/// Draws a rectangle behind each path item,
26/// requires path items to have transparent backgrounds,
27/// useful when you want uniform path styling.
28///
29/// The downside is,
30/// depending on the style you're going for,
31/// oftentimes manually syncing the path's styling to the path items' is necessary,
32/// the default styling simply can't cover most use cases.
33#[derive(Debug, Clone, Copy)]
34pub enum DrawPath {
35    /// FakeHovering
36    FakeHovering,
37    /// Backdrop
38    Backdrop,
39}
40
41/// X+ goes right and Y+ goes down
42#[derive(Debug, Clone, Copy)]
43pub(super) enum Direction {
44    Positive,
45    Negative,
46}
47impl Direction {
48    pub(super) fn flip(self) -> Self {
49        match self {
50            Self::Positive => Self::Negative,
51            Self::Negative => Self::Positive,
52        }
53    }
54}
55
56/// Axis
57#[allow(missing_docs)]
58#[derive(Debug, Clone, Copy)]
59pub(super) enum Axis {
60    Horizontal,
61    Vertical,
62}
63
64pub(super) type Index = Option<usize>;
65
66/// Should be returned from the recursive event processing function,
67/// tells the caller which type of event has been processed
68///
69/// `Event`: The child event has been processed.
70/// The parent menu should not process the event.
71///
72/// `Close`: Either the child menu has decided to close itself,
73/// or that there is no child menu open,
74/// from the parent menu's perspective,
75/// there is no difference between the two.
76/// The parent menu should check if it should close itself,
77/// if not then it should process the event.
78///
79/// `None`: A child menu is open, but it did not process the event,
80/// this happens when the cursor hovers over the item that opens the child menu
81/// but has not entered the child menu yet,
82/// in this case the parent menu should process the event,
83/// but close check is not needed.
84///
85#[derive(Debug, Clone, Copy)]
86pub(super) enum RecEvent {
87    Event,
88    Close,
89    None,
90}
91
92#[derive(Debug, Clone, Copy)]
93/// Scroll speed
94pub struct ScrollSpeed {
95    /// Speed of line-based scroll movement
96    pub line: f32,
97    /// Speed of Pixel scroll movement
98    pub pixel: f32,
99}
100
101pub fn pad_rectangle(rect: Rectangle, padding: Padding) -> Rectangle {
102    Rectangle {
103        x: rect.x - padding.left,
104        y: rect.y - padding.top,
105        width: rect.width + padding.x(),
106        height: rect.height + padding.y(),
107    }
108}
109
110#[derive(Debug, Clone, Copy)]
111pub(super) struct MenuSlice {
112    pub(super) start_index: usize,
113    pub(super) end_index: usize,
114    pub(super) lower_bound_rel: f32,
115    pub(super) upper_bound_rel: f32,
116}
117impl MenuSlice {
118    pub(super) fn from_bounds_rel(
119        lower_bound_rel: f32,
120        upper_bound_rel: f32,
121        items_node: &Node,
122        get_position: fn(&Node) -> f32,
123    ) -> Self {
124        let max_index = items_node.children().len().saturating_sub(1);
125        let nodes = items_node.children();
126        let start_index = search_bound(0, max_index, lower_bound_rel, nodes, get_position);
127        let end_index = search_bound(start_index, max_index, upper_bound_rel, nodes, get_position);
128
129        Self {
130            start_index,
131            end_index,
132            lower_bound_rel,
133            upper_bound_rel,
134        }
135    }
136}
137
138/* fn search_bound_lin(
139    bound: f32,
140    nodes: &[Node],
141    mut start_index: usize, // should be less than nodes.len()-1
142) -> usize{
143    for (i, n) in nodes.iter().enumerate().skip(start_index){
144        let b = n.bounds();
145        if !(bound > b.y + b.height){
146            start_index = i;
147            break;
148        }
149    }
150    start_index
151} */
152
153pub(super) fn search_bound(
154    default_left: usize,
155    default_right: usize,
156    bound: f32,
157    list: &[Node],
158    get_position: fn(&Node) -> f32,
159) -> usize {
160    // binary search
161    let mut left = default_left;
162    let mut right = default_right;
163
164    while left != right {
165        let m = usize::midpoint(left, right) + 1;
166        if get_position(&list[m]) > bound {
167            right = m - 1;
168        } else {
169            left = m;
170        }
171    }
172    left
173}
174
175pub(super) fn clip_node_y(node: &Node, height: f32, offset: f32) -> Node {
176    let node_bounds = node.bounds();
177    Node::with_children(
178        Size::new(node_bounds.width, height),
179        node.children()
180            .iter()
181            .map(|n| n.clone().translate([0.0, -offset]))
182            .collect(),
183    )
184    .move_to(node_bounds.position())
185    .translate([0.0, offset])
186}
187
188pub(super) fn clip_node_x(node: &Node, width: f32, offset: f32) -> Node {
189    let node_bounds = node.bounds();
190    Node::with_children(
191        Size::new(width, node_bounds.height),
192        node.children()
193            .iter()
194            .map(|n| n.clone().translate([-offset, 0.0]))
195            .collect(),
196    )
197    .move_to(node_bounds.position())
198    .translate([offset, 0.0])
199}
200
201/// Parameters that are shared by all menus in the menu bar
202pub(super) struct GlobalParameters<'a, Theme: crate::style::menu_bar::Catalog> {
203    pub(super) safe_bounds_margin: f32,
204    pub(super) draw_path: DrawPath,
205    pub(super) scroll_speed: ScrollSpeed,
206    pub(super) close_on_item_click: bool,
207    pub(super) close_on_background_click: bool,
208    pub(super) class: Theme::Class<'a>,
209}
210
211/// Tries to open a menu at the given cursor position
212pub(super) fn try_open_menu<'a, 'b, Message, Theme: Catalog, Renderer: renderer::Renderer>(
213    items: &mut [Item<'a, Message, Theme, Renderer>],
214    menu_state: &mut MenuState,
215    item_trees: &mut [Tree],
216    item_layouts: impl Iterator<Item = Layout<'b>>,
217    cursor: mouse::Cursor,
218    shell: &mut Shell<'_, Message>,
219) {
220    let old_active = menu_state.active;
221    let slice = menu_state.slice;
222
223    for (i, ((item, tree), layout)) in
224        itl_iter_slice_enum!(slice, items;iter_mut, item_trees;iter_mut, item_layouts)
225    {
226        if cursor.is_over(layout.bounds()) {
227            if item.menu.is_some() {
228                menu_state.open_new_menu(i, item, tree);
229            }
230            break;
231        }
232    }
233
234    if menu_state.active != old_active {
235        shell.invalidate_layout();
236        shell.request_redraw();
237    }
238}
239
240/// Schedules a close on click task if applicable
241///
242/// This function assumes that a mouse::Event::ButtonPressed(mouse::Button::Left) event has occurred,
243/// make sure to check the event before calling this function.
244#[allow(clippy::too_many_arguments)]
245pub(super) fn schedule_close_on_click<
246    'a,
247    'b,
248    Message,
249    Theme: Catalog,
250    Renderer: renderer::Renderer,
251>(
252    global_state: &mut GlobalState,
253    global_parameters: &GlobalParameters<'_, Theme>,
254    slice: MenuSlice,
255    items: &mut [Item<'a, Message, Theme, Renderer>],
256    slice_layout: impl Iterator<Item = Layout<'b>>,
257    cursor: mouse::Cursor,
258    menu_coic: Option<bool>,
259    menu_cobc: Option<bool>,
260) {
261    global_state.clear_task();
262
263    let mut coc_handled = false;
264
265    for (item, layout) in items[slice.start_index..=slice.end_index] // [item...]
266        .iter_mut()
267        .zip(slice_layout)
268    {
269        if cursor.is_over(layout.bounds()) {
270            if let Some(coc) = item.close_on_click {
271                coc_handled = true;
272                if coc {
273                    global_state.schedule(MenuBarTask::CloseOnClick);
274                }
275            }
276            for cocfb in [menu_coic, Some(global_parameters.close_on_item_click)] {
277                if let (false, Some(coc)) = (coc_handled, cocfb) {
278                    coc_handled = true;
279                    if coc {
280                        global_state.schedule(MenuBarTask::CloseOnClick);
281                    }
282                }
283            }
284            break;
285        }
286    }
287
288    for cocfb in [menu_cobc, Some(global_parameters.close_on_background_click)] {
289        if let (false, Some(coc)) = (coc_handled, cocfb) {
290            coc_handled = true;
291            if coc {
292                global_state.schedule(MenuBarTask::CloseOnClick);
293            }
294        }
295    }
296}
297
298macro_rules! itl_iter_slice {
299    ($slice:expr, $items:expr;$iter_0:ident, $item_trees:expr;$iter_1:ident, $slice_layout:expr) => {
300        $items[$slice.start_index..=$slice.end_index]
301            .$iter_0()
302            .zip($item_trees[$slice.start_index..=$slice.end_index].$iter_1())
303            .zip($slice_layout)
304    };
305}
306pub(super) use itl_iter_slice;
307
308macro_rules! itl_iter_slice_enum {
309    ($slice:expr, $items:expr;$iter_0:ident, $item_trees:expr;$iter_1:ident, $slice_layout:expr) => {
310        itl_iter_slice!($slice, $items;$iter_0, $item_trees;$iter_1, $slice_layout)
311            .enumerate()
312            .map(move |(i, ((item, tree), layout))| (i + $slice.start_index, ((item, tree), layout)))
313    };
314}
315pub(super) use itl_iter_slice_enum;