use crate::geometry::{LogicalPoint, LogicalRect, LogicalSize};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FlyoutSide {
Right,
Left,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FlyoutPlacement {
pub origin: LogicalPoint,
pub side: FlyoutSide,
}
pub fn place_flyout(
parent: LogicalRect,
row: LogicalRect,
flyout: LogicalSize,
work_area: LogicalRect,
) -> FlyoutPlacement {
let right_x = parent.max_x();
let fits_right = right_x + flyout.width <= work_area.max_x();
let (side, mut x) = if fits_right {
(FlyoutSide::Right, right_x)
} else {
(FlyoutSide::Left, parent.origin.x - flyout.width)
};
let max_x = (work_area.max_x() - flyout.width).max(work_area.origin.x);
x = x.clamp(work_area.origin.x, max_x);
let mut y = parent.origin.y + row.origin.y;
let max_y = (work_area.max_y() - flyout.height).max(work_area.origin.y);
y = y.clamp(work_area.origin.y, max_y);
FlyoutPlacement {
origin: LogicalPoint::new(x, y),
side,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HoverTarget {
ParentRow {
panel: usize,
index: usize,
},
OtherRow {
panel: usize,
},
Flyout,
Outside,
}
pub fn next_flyout(current: &[usize], target: HoverTarget) -> Vec<usize> {
match target {
HoverTarget::ParentRow { panel, index } => {
if current.get(panel) == Some(&index) {
current.to_vec()
} else {
let keep = panel.min(current.len());
let mut next = current[..keep].to_vec();
next.push(index);
next
}
}
HoverTarget::OtherRow { panel } => {
let keep = panel.min(current.len());
current[..keep].to_vec()
}
HoverTarget::Flyout | HoverTarget::Outside => current.to_vec(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rect(x: f32, y: f32, w: f32, h: f32) -> LogicalRect {
LogicalRect::new(LogicalPoint::new(x, y), LogicalSize::new(w, h))
}
fn screen() -> LogicalRect {
rect(0.0, 0.0, 1440.0, 900.0)
}
#[test]
fn opens_to_the_right_flush_and_row_aligned() {
let parent = rect(100.0, 100.0, 200.0, 300.0);
let row = rect(0.0, 40.0, 200.0, 22.0); let p = place_flyout(parent, row, LogicalSize::new(180.0, 150.0), screen());
assert_eq!(p.side, FlyoutSide::Right);
assert_eq!(p.origin.x, 300.0);
assert_eq!(p.origin.y, 140.0);
}
#[test]
fn flips_left_when_no_room_on_the_right() {
let parent = rect(1300.0, 100.0, 130.0, 300.0);
let row = rect(0.0, 0.0, 130.0, 22.0);
let p = place_flyout(parent, row, LogicalSize::new(180.0, 150.0), screen());
assert_eq!(p.side, FlyoutSide::Left);
assert_eq!(p.origin.x, 1120.0);
}
#[test]
fn flip_left_is_clamped_into_the_work_area() {
let work = rect(0.0, 0.0, 300.0, 900.0);
let parent = rect(250.0, 0.0, 50.0, 300.0);
let row = rect(0.0, 0.0, 50.0, 22.0);
let p = place_flyout(parent, row, LogicalSize::new(280.0, 150.0), work);
assert_eq!(p.side, FlyoutSide::Left);
assert_eq!(p.origin.x, 0.0); }
#[test]
fn clamps_to_the_bottom_of_the_work_area() {
let parent = rect(100.0, 800.0, 200.0, 80.0);
let row = rect(0.0, 40.0, 200.0, 22.0); let p = place_flyout(parent, row, LogicalSize::new(180.0, 200.0), screen());
assert_eq!(p.origin.y, 700.0);
}
#[test]
fn clamps_to_the_top_of_the_work_area() {
let work = rect(0.0, 25.0, 1440.0, 875.0);
let parent = rect(100.0, 10.0, 200.0, 300.0);
let row = rect(0.0, 0.0, 200.0, 22.0); let p = place_flyout(parent, row, LogicalSize::new(180.0, 150.0), work);
assert_eq!(p.origin.y, 25.0);
}
#[test]
fn hover_parent_opens_and_switches() {
assert_eq!(
next_flyout(&[], HoverTarget::ParentRow { panel: 0, index: 2 }),
vec![2]
);
assert_eq!(
next_flyout(&[2], HoverTarget::ParentRow { panel: 0, index: 5 }),
vec![5]
);
}
#[test]
fn hover_other_row_closes_the_flyout() {
assert_eq!(
next_flyout(&[2], HoverTarget::OtherRow { panel: 0 }),
Vec::<usize>::new()
);
assert_eq!(
next_flyout(&[], HoverTarget::OtherRow { panel: 0 }),
Vec::<usize>::new()
);
}
#[test]
fn moving_into_flyout_keeps_it_open() {
assert_eq!(next_flyout(&[2], HoverTarget::Flyout), vec![2]);
}
#[test]
fn drifting_outside_keeps_current() {
assert_eq!(next_flyout(&[2], HoverTarget::Outside), vec![2]);
assert_eq!(next_flyout(&[], HoverTarget::Outside), Vec::<usize>::new());
}
#[test]
fn hover_submenu_row_in_flyout_opens_deeper_level() {
assert_eq!(
next_flyout(&[2], HoverTarget::ParentRow { panel: 1, index: 4 }),
vec![2, 4]
);
}
#[test]
fn rehovering_open_parent_keeps_deeper_levels() {
assert_eq!(
next_flyout(&[2, 4, 1], HoverTarget::ParentRow { panel: 1, index: 4 }),
vec![2, 4, 1]
);
}
#[test]
fn hover_shallower_panel_closes_deeper_flyouts() {
assert_eq!(
next_flyout(&[2, 4], HoverTarget::OtherRow { panel: 0 }),
Vec::<usize>::new()
);
assert_eq!(
next_flyout(&[2, 4], HoverTarget::ParentRow { panel: 1, index: 6 }),
vec![2, 6]
);
}
#[test]
fn place_flyout_chains_relative_to_the_previous_panel() {
let popup = rect(100.0, 100.0, 200.0, 300.0);
let row1 = rect(0.0, 40.0, 200.0, 22.0);
let l1 = place_flyout(popup, row1, LogicalSize::new(180.0, 150.0), screen());
assert_eq!(l1.side, FlyoutSide::Right);
let l1_rect = LogicalRect::new(l1.origin, LogicalSize::new(180.0, 150.0));
let row2 = rect(0.0, 20.0, 180.0, 22.0);
let l2 = place_flyout(l1_rect, row2, LogicalSize::new(160.0, 120.0), screen());
assert_eq!(l2.side, FlyoutSide::Right);
assert_eq!(l2.origin.x, l1_rect.max_x());
assert_eq!(l2.origin.y, l1.origin.y + 20.0);
}
#[test]
fn keyboard_and_mouse_drive_the_same_open_flyout_state() {
use crate::keynav::{handle_key, MenuFocus, NavKey};
use crate::menu::{Menu, Row};
let menu = Menu::new()
.row(Row::new("a").label("Apple"))
.submenu(
Row::new("settings").label("Settings"),
Menu::new().row(Row::new("s1").label("One")),
)
.row(Row::new("quit").label("Quit"));
let submenu_index = 1;
let mut focus = MenuFocus {
top: Some(submenu_index),
flyout: Vec::new(),
};
handle_key(&menu, &mut focus, NavKey::Right);
let keyboard_open: Vec<usize> = focus.flyout.iter().map(|fly| fly.parent).collect();
let mouse_open = next_flyout(
&[],
HoverTarget::ParentRow {
panel: 0,
index: submenu_index,
},
);
assert_eq!(keyboard_open, vec![submenu_index]);
assert_eq!(mouse_open, vec![submenu_index]);
assert_eq!(keyboard_open, mouse_open);
}
}