use crate::ui::prelude::*;
use crate::ui::ViewState;
use std::hash::Hash;
#[derive(Debug, Eq, PartialEq)]
pub(super) struct MenuItemView<Key: Hash + Copy + PartialEq + Eq + Debug> {
pub(super) id: Key,
pub(super) item_bounds: Rect,
pub(super) name: String,
pub(super) state: ViewState,
pub(super) focused: bool,
pub(super) content: ItemContent<Key>,
}
#[derive(Debug, Eq, PartialEq)]
pub(super) enum ItemContent<Key: Hash + Copy + PartialEq + Eq + Debug> {
Button,
Checkable(bool),
Parent(Vec<MenuItemView<Key>>, ChildrenAnchor, Rect),
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub(super) enum ChildrenAnchor {
Bottom,
Side,
}
impl<Key: Hash + Copy + PartialEq + Eq + Debug> MenuItemView<Key> {
pub(super) fn new(item: &MenuBarItem<Key>, is_root: bool) -> Self {
let anchor = if is_root {
ChildrenAnchor::Bottom
} else {
ChildrenAnchor::Side
};
let content = if let Some(default) = item.checkable {
ItemContent::Checkable(default)
} else if let Some(items) = &item.children {
let children: Vec<MenuItemView<Key>> =
items.iter().map(|c| MenuItemView::new(c, false)).collect();
ItemContent::Parent(children, anchor, Rect::new((0, 0), (0, 0)))
} else {
ItemContent::Button
};
Self {
id: item.id,
item_bounds: Rect::new((0, 0), (0, 0)),
name: item.name.clone(),
state: ViewState::Normal,
focused: false,
content,
}
}
}
impl<Key: Hash + Copy + PartialEq + Eq + Debug> MenuItemView<Key> {
pub(super) fn on_mouse_move(&mut self, xy: Coord) {
self.focused = self.item_bounds.contains(xy) || (self.focused && self.child_expanded(xy))
}
fn child_expanded(&mut self, xy: Coord) -> bool {
match &mut self.content {
ItemContent::Checkable(_) | ItemContent::Button => false,
ItemContent::Parent(items, _, _) => {
for child in items.iter_mut() {
child.on_mouse_move(xy);
}
items.iter().any(|c| c.focused)
}
}
}
}
pub(super) fn draw_titles<Key: Hash + Copy + PartialEq + Eq + Debug>(
graphics: &mut Graphics,
mouse: Coord,
style: &MenuBarStyle,
items: &[MenuItemView<Key>],
) {
for title in items {
let col = style.menu_item.font.px_to_cols(title.item_bounds.width());
let (err, dis) = title.state.get_err_dis();
let hover = title.item_bounds.contains(mouse);
if let Some(clr) = style
.menu_item
.background
.get(hover, title.focused, err, dis)
{
graphics.draw_rect(title.item_bounds.clone(), fill(clr));
}
if let Some(clr) = style.menu_item.text.get(hover, title.focused, err, dis) {
let str = match title.content {
ItemContent::Checkable(true) => "✓",
ItemContent::Checkable(false) => " ",
_ => "",
};
graphics.draw_text(
&format!("{str}{}", title.name),
TextPos::px(title.item_bounds.top_left() + style.menu_item.padding.offset()),
(clr, style.menu_item.font, WrappingStrategy::AtCol(col)),
);
}
draw(graphics, mouse, style, title);
}
}
fn draw<Key: Hash + Copy + PartialEq + Eq + Debug>(
graphics: &mut Graphics,
mouse: Coord,
style: &MenuBarStyle,
item: &MenuItemView<Key>,
) {
if item.focused && item.state == ViewState::Normal {
if let Some(bg) = style.menu_item.dropdown_background {
match &item.content {
ItemContent::Button | ItemContent::Checkable(_) => {}
ItemContent::Parent(_, _, bounds) => graphics.draw_rect(bounds.clone(), fill(bg)),
}
}
match &item.content {
ItemContent::Checkable(_) | ItemContent::Button => {}
ItemContent::Parent(items, _, dropdown_bounds) => {
let col = style.dropdown_item.font.px_to_cols(dropdown_bounds.width());
let any_checkable = items
.iter()
.any(|c| matches!(c.content, ItemContent::Checkable(_)));
for child in items {
let (err, dis) = child.state.get_err_dis();
if let Some(clr) = style.dropdown_item.background.get(
child.item_bounds.contains(mouse),
child.focused,
err,
dis,
) {
graphics.draw_rect(child.item_bounds.clone(), fill(clr));
}
if let Some(clr) = style.dropdown_item.text.get(
child.item_bounds.contains(mouse),
child.focused,
err,
dis,
) {
let str = match (any_checkable, &child.content) {
(true, ItemContent::Checkable(true)) => "✓",
(true, _) => " ",
(_, _) => "",
};
graphics.draw_text(
&format!("{str}{}", &child.name),
TextPos::px(
child.item_bounds.top_left() + style.dropdown_item.padding.offset(),
),
(clr, style.dropdown_item.font, WrappingStrategy::Cutoff(col)),
);
}
if matches!(&child.content, ItemContent::Parent(_, _, _)) {
if let Some(icon) = style.dropdown_item.arrow.get(
child.item_bounds.contains(mouse),
child.focused,
err,
dis,
) {
graphics.draw_indexed_image(
child.item_bounds.top_right() - (icon.width() as usize, 0)
+ (
0,
(child.item_bounds.height() / 2)
- (icon.height() as usize / 2),
),
icon,
);
}
draw(graphics, mouse, style, child);
}
}
}
}
}
}
pub(super) fn collapse_menu<Key: Hash + Copy + PartialEq + Eq + Debug>(
children: &mut [MenuItemView<Key>],
) {
children.iter_mut().for_each(|c| {
if let ItemContent::Parent(items, _, _) = &mut c.content {
collapse_menu(items);
}
c.focused = false;
});
}
pub(super) fn on_click_path<Key: Hash + Copy + PartialEq + Eq + Debug>(
items: &[MenuItemView<Key>],
down_at: Coord,
up_at: Coord,
) -> Option<Key> {
for item in items {
if item.focused && item.state == ViewState::Normal {
if item.item_bounds.contains(down_at) && item.item_bounds.contains(up_at) {
return Some(item.id);
} else if let ItemContent::Parent(children, _, _) = &item.content {
let result = on_click_path(children, down_at, up_at);
if result.is_some() {
return result;
}
}
}
}
None
}
pub(super) fn focused_bounds<Key: Hash + Copy + PartialEq + Eq + Debug>(
item: &MenuItemView<Key>,
) -> Option<Rect> {
if item.focused && item.state == ViewState::Normal {
let mut bounds = item.item_bounds.clone();
match &item.content {
ItemContent::Checkable(_) | ItemContent::Button => {}
ItemContent::Parent(items, _, child_bounds) => {
bounds = union(&bounds, child_bounds);
for child in items {
if let Some(extra) = focused_bounds(child) {
bounds = union(&bounds, &extra);
}
}
}
}
Some(bounds)
} else {
None
}
}
pub(super) fn union(lhs: &Rect, rhs: &Rect) -> Rect {
Rect::new(
(lhs.left().min(rhs.left()), lhs.top().min(rhs.top())),
(lhs.right().max(rhs.right()), lhs.bottom().max(rhs.bottom())),
)
}
pub(super) fn layout_titles<Key: Hash + Copy + PartialEq + Eq + Debug>(
top_left: Coord,
style: &MenuBarStyle,
screen_size: (usize, usize),
fill_width: bool,
items: &mut [MenuItemView<Key>],
) -> Rect {
let mut bounds = Rect::new_with_size(top_left, if fill_width { screen_size.0 } else { 0 }, 0);
let mut start = top_left;
for item in items {
let (w, h) = style.menu_item.font.measure(&item.name);
item.item_bounds = Rect::new_with_size(
start,
w + style.menu_item.padding.horz(),
h + style.menu_item.padding.vert(),
);
start = item.item_bounds.top_right() + (1, 0);
bounds = union(&bounds, &item.item_bounds);
match &mut item.content {
ItemContent::Checkable(_) | ItemContent::Button => {}
ItemContent::Parent(_, _, _) => layout_children(item, style, screen_size),
}
}
bounds
}
#[allow(clippy::only_used_in_recursion)] fn layout_children<Key: Hash + Copy + PartialEq + Eq + Debug>(
item: &mut MenuItemView<Key>,
style: &MenuBarStyle,
screen_size: (usize, usize),
) {
match &mut item.content {
ItemContent::Checkable(_) | ItemContent::Button => {}
ItemContent::Parent(items, anchor, bounds) => {
let min_right = if anchor == &ChildrenAnchor::Bottom {
item.item_bounds.right().max(0) as usize
} else {
0
};
let any_checks = items
.iter()
.any(|v| matches!(v.content, ItemContent::Checkable(_)));
let any_submenus = items
.iter()
.any(|v| matches!(v.content, ItemContent::Parent(_, _, _)));
let w = items
.iter()
.map(|v| {
let mut w = style.dropdown_item.font.measure(&v.name).0;
if any_checks {
w += style.dropdown_item.font.char_width();
}
if any_submenus {
w += style.dropdown_item.font.char_width();
}
w
})
.max()
.unwrap_or_default();
let mut start = if anchor == &ChildrenAnchor::Bottom {
item.item_bounds.bottom_left() + (0, 1)
} else if item.item_bounds.top_right().x + w as isize > screen_size.0 as isize
&& item.item_bounds.top_left().x - w as isize >= 0
{
item.item_bounds.top_left() - (w, 0) - (style.dropdown_item.padding.horz(), 0)
} else {
item.item_bounds.top_right()
};
let mut container_bounds = Rect::new_with_size(start, w, 0);
for child in items {
let (_, h) = style.dropdown_item.font.measure(&child.name);
child.item_bounds = Rect::new(
start,
(
(start.x + w as isize + style.dropdown_item.padding.horz() as isize)
.max(min_right as isize),
start.y + h as isize + style.dropdown_item.padding.vert() as isize,
),
);
start = child.item_bounds.bottom_left() + (0, 1);
container_bounds = union(&container_bounds, &child.item_bounds);
layout_children(child, style, screen_size);
}
*bounds = Rect::new(
container_bounds.top_left(),
(
container_bounds.bottom_right().x.max(min_right as isize),
container_bounds.bottom_right().y,
),
);
}
}
}