Skip to content

Commit

Permalink
Add menu close condition (#100)
Browse files Browse the repository at this point in the history
* Add menu close condition

* Exclude click and drag

* fmt
  • Loading branch information
latidoremi authored Mar 6, 2023
1 parent 1aaa3e4 commit b9a7692
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 54 deletions.
9 changes: 7 additions & 2 deletions examples/menu/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use iced::widget::{
};
use iced::{alignment, theme, Application, Color, Element, Length};

use iced_aw::menu::{ItemHeight, ItemWidth, MenuBar, MenuTree, PathHighlight};
use iced_aw::menu::{CloseCondition, ItemHeight, ItemWidth, MenuBar, MenuTree, PathHighlight};
use iced_aw::quad;

pub fn main() -> iced::Result {
Expand Down Expand Up @@ -178,7 +178,12 @@ impl Application for App {
}
.spacing(4.0)
.bounds_expand(30)
.path_highlight(Some(PathHighlight::MenuActive));
.path_highlight(Some(PathHighlight::MenuActive))
.close_condition(CloseCondition {
leave: true,
click_outside: false,
click_inside: false,
});

let r = row!(mb, horizontal_space(Length::Fill), pick_size_option)
.padding([2, 8])
Expand Down
2 changes: 1 addition & 1 deletion src/native/menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,5 @@ mod menu_tree;

pub use crate::style::menu_bar::{Appearance, StyleSheet};
pub use menu_bar::MenuBar;
pub use menu_inner::{ItemHeight, ItemWidth, PathHighlight};
pub use menu_inner::{CloseCondition, ItemHeight, ItemWidth, PathHighlight};
pub use menu_tree::MenuTree;
16 changes: 15 additions & 1 deletion src/native/menu/menu_bar.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! A widget that handles menu trees

use super::menu_inner::{ItemHeight, ItemWidth, Menu, MenuState, PathHighlight};
use super::menu_inner::{CloseCondition, ItemHeight, ItemWidth, Menu, MenuState, PathHighlight};
use super::menu_tree::MenuTree;
use crate::style::menu_bar::StyleSheet;
use iced_native::widget::{tree, Tree};
Expand Down Expand Up @@ -55,6 +55,7 @@ where
spacing: f32,
padding: Padding,
bounds_expand: u16,
close_condition: CloseCondition,
item_width: ItemWidth,
item_height: ItemHeight,
path_highlight: Option<PathHighlight>,
Expand All @@ -78,6 +79,11 @@ where
spacing: 0.0,
padding: Padding::ZERO,
bounds_expand: 15,
close_condition: CloseCondition {
leave: true,
click_outside: true,
click_inside: true,
},
item_width: ItemWidth::Uniform(150),
item_height: ItemHeight::Uniform(30),
path_highlight: Some(PathHighlight::MenuActive),
Expand Down Expand Up @@ -125,6 +131,13 @@ where
self
}

/// [`CloseCondition`]
#[must_use]
pub fn close_condition(mut self, close_condition: CloseCondition) -> Self {
self.close_condition = close_condition;
self
}

/// [`ItemWidth`]
#[must_use]
pub fn item_width(mut self, item_width: ItemWidth) -> Self {
Expand Down Expand Up @@ -358,6 +371,7 @@ where
tree,
menu_roots: &mut self.menu_roots,
bounds_expand: self.bounds_expand,
close_condition: self.close_condition,
item_width: self.item_width,
item_height: self.item_height,
bar_bounds: layout.bounds(),
Expand Down
139 changes: 89 additions & 50 deletions src/native/menu/menu_inner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ use iced_native::{
Shell, Size,
};

/// The condition of when to close a menu
#[derive(Debug, Clone, Copy)]
pub struct CloseCondition {
/// Close menus when the cursor moves outside the check bounds
pub leave: bool,

/// Close menus when the cursor clicks outside the check bounds
pub click_outside: bool,

/// Close menus when the cursor clicks inside the check bounds
pub click_inside: bool,
}

/// The width of an item
#[derive(Debug, Clone, Copy)]
pub enum ItemWidth {
Expand Down Expand Up @@ -74,20 +87,12 @@ impl MenuBounds {
Renderer: renderer::Renderer,
{
let children_size = get_children_size(menu_tree, item_width, item_height);

let (children_position, mask) =
let children_position =
adaptive_open_direction(parent_bounds, children_size, viewport, aod_settings);

let children_bounds = Rectangle::new(children_position, children_size);

let mut padding = [0; 4];
padding.iter_mut().enumerate().for_each(|(i, p)| {
*p = mask[i] * bounds_expand;
});

let child_positions = get_child_positions(menu_tree, item_height);
let check_bounds = pad_rectangle(children_bounds, [bounds_expand; 4].into());

let check_bounds = pad_rectangle(children_bounds, padding.into());
Self {
child_positions,
children_bounds,
Expand Down Expand Up @@ -241,6 +246,7 @@ where
pub(super) tree: &'b mut Tree,
pub(super) menu_roots: &'b mut Vec<MenuTree<'a, Message, Renderer>>,
pub(super) bounds_expand: u16,
pub(super) close_condition: CloseCondition,
pub(super) item_width: ItemWidth,
pub(super) item_height: ItemHeight,
pub(super) bar_bounds: Rectangle,
Expand Down Expand Up @@ -310,28 +316,46 @@ where
process_scroll_events(self, delta, viewport).merge(menu_status)
}

Mouse(CursorMoved { position }) | Touch(FingerMoved { position, .. }) => {
process_overlay_events(self, viewport, position).merge(menu_status)
}

Mouse(ButtonPressed(Left)) | Touch(FingerPressed { .. }) => {
let state = self.tree.state.downcast_mut::<MenuBarState>();
state.pressed = true;
state.cursor = cursor_position;
Captured
}

Mouse(CursorMoved { position }) | Touch(FingerMoved { position, .. }) => {
process_overlay_events(self, viewport, position).merge(menu_status)
}

Mouse(ButtonReleased(Left)) | Touch(FingerLifted { .. }) => {
let state = self.tree.state.downcast_mut::<MenuBarState>();
state.pressed = false;

if state.cursor.distance(cursor_position) < 2.0 {
let is_inside = state
.menu_states
.iter()
.any(|ms| ms.menu_bounds.check_bounds.contains(cursor_position));

if self.close_condition.click_inside && is_inside {
state.reset();
return Captured;
}

if self.close_condition.click_outside && !is_inside {
state.reset();
return Captured;
}
}

if self.bar_bounds.contains(cursor_position) {
state.reset();
state.cursor = cursor_position;
Captured
} else {
menu_status
}
}

_ => menu_status,
}
}
Expand Down Expand Up @@ -553,21 +577,17 @@ where
{
use event::Status::{Captured, Ignored};
/*
if no active root || pressed:
if no active root || pressed:
return
else:
remove invalid menu
remove invalid menus
update active item
if active item is a menu:
add menu
*/

let state = menu.tree.state.downcast_mut::<MenuBarState>();

/* When overlay is running, cursor_position in any widget method will go negative
but I still want Widget::draw() to react to cursor movement */
state.cursor = position;

let Some(active_root) = state.active_root else{
if !menu.bar_bounds.contains(position){
state.reset();
Expand All @@ -579,14 +599,43 @@ where
return Ignored;
}

/* When overlay is running, cursor_position in any widget method will go negative
but I still want Widget::draw() to react to cursor movement */
state.cursor = position;

// remove invalid menus
for i in (0..state.menu_states.len()).rev() {
let mb = &state.menu_states[i].menu_bounds;
let mut prev_bounds = std::iter::once(menu.bar_bounds)
.chain(
state.menu_states[..state.menu_states.len().saturating_sub(1)]
.iter()
.map(|ms| ms.menu_bounds.children_bounds),
)
.collect::<Vec<_>>();

if mb.parent_bounds.contains(position) || mb.check_bounds.contains(position) {
break;
if menu.close_condition.leave {
for i in (0..state.menu_states.len()).rev() {
let mb = &state.menu_states[i].menu_bounds;

if (mb.parent_bounds.contains(position) || mb.check_bounds.contains(position))
&& prev_bounds.iter().all(|pvb| !pvb.contains(position))
{
break;
}
prev_bounds.pop();
state.menu_states.pop();
}
} else {
for i in (0..state.menu_states.len()).rev() {
let mb = &state.menu_states[i].menu_bounds;

if mb.parent_bounds.contains(position)
|| prev_bounds.iter().all(|pvb| !pvb.contains(position))
{
break;
}
prev_bounds.pop();
state.menu_states.pop();
}
state.menu_states.pop();
}

// get indices
Expand All @@ -613,9 +662,13 @@ where
let last_menu_bounds = &last_menu_state.menu_bounds;
let last_parent_bounds = last_menu_bounds.parent_bounds;
let last_child_bounds = last_menu_bounds.children_bounds;
let last_check_bounds = last_menu_bounds.check_bounds;

if last_parent_bounds.contains(position) {
// cursor is in the parent part
if last_parent_bounds.contains(position)
// cursor is in the parent part
|| !last_check_bounds.contains(position)
// cursor is outside
{
last_menu_state.index = None;
return Captured;
}
Expand All @@ -625,7 +678,6 @@ where
let height_diff = (position.y - (last_child_bounds.y + last_menu_state.scroll_offset))
.clamp(0.0, last_child_bounds.height - 0.001);

// get active menu
let active_menu_root = &menu.menu_roots[active_root];

let active_menu = indices[0..indices.len().saturating_sub(1)]
Expand All @@ -634,7 +686,6 @@ where
&mt.children[i.expect("missing active child index in menu")]
});

// get new index
let new_index = match menu.item_height {
ItemHeight::Uniform(u) => (height_diff / f32::from(u)).floor() as usize,
ItemHeight::Static(s) => {
Expand Down Expand Up @@ -696,7 +747,7 @@ fn adaptive_open_direction(
children_size: Size,
bounds: Size,
setttings: [bool; 4],
) -> (Point, [u16; 4]) {
) -> Point {
/*
Imagine there're two sticks, parent and child
parent: o-----o
Expand Down Expand Up @@ -728,42 +779,35 @@ fn adaptive_open_direction(
let [horizontal, vertical, horizontal_overlap, vertical_overlap] = setttings;

let calc_adaptive = |parent_pos, parent_size, child_size, max_size, on, overlap| {
let mut padding_mask = [1, 1];

if on {
let space_left = parent_pos;
let space_right = max_size - parent_pos - parent_size;

if space_left > space_right && child_size > space_right {
let value = if overlap {
return if overlap {
parent_pos - child_size + parent_size
} else {
padding_mask[1] = 0;
parent_pos - child_size
};
return (value, padding_mask);
}
}

let value = if overlap {
if overlap {
parent_pos
} else {
padding_mask[0] = 0;
parent_pos + parent_size
};

(value, padding_mask)
}
};

let (x, px) = calc_adaptive(
let x = calc_adaptive(
parent_bounds.x,
parent_bounds.width,
children_size.width,
bounds.width,
horizontal,
horizontal_overlap,
);
let (y, py) = calc_adaptive(
let y = calc_adaptive(
parent_bounds.y,
parent_bounds.height,
children_size.height,
Expand All @@ -772,9 +816,7 @@ fn adaptive_open_direction(
vertical_overlap,
);

let padding_mask = [py[0], px[1], py[1], px[0]];

([x, y].into(), padding_mask)
[x, y].into()
}

fn process_scroll_events<Message, Renderer>(
Expand Down Expand Up @@ -807,10 +849,7 @@ where
if state.menu_states.is_empty() {
return Ignored;
} else if state.menu_states.len() == 1 {
let last_ms = state
.menu_states
.last_mut()
.expect("missing menu state item");
let last_ms = &mut state.menu_states[0];
let (max_offset, min_offset) = calc_offset_bounds(last_ms, viewport);
last_ms.scroll_offset = (last_ms.scroll_offset + delta_y).clamp(min_offset, max_offset);
} else {
Expand Down

0 comments on commit b9a7692

Please sign in to comment.