use super::{
events::*, menu_button_state::MenuButtonState, menu_item::MenuItem, mouse_position_info::MousePositionInfo, CheckBox, Command, MenuItemWrapper,
Separator, SingleChoice, SubMenu,
};
use crate::{
graphics::{Character, ClipArea, LineType, Rect, Size, SpecialChar, Surface, TextAlignment, TextFormatBuilder, WrapType},
input::{Key, KeyCode, MouseWheelDirection},
prelude::KeyModifier,
system::{Handle, HandleSupport, RuntimeManager, Theme},
ui::common::traits::EventProcessStatus,
utils::{Strategy, VectorIndex},
};
use appcui_proc_macro::key;
use std::sync::atomic::{AtomicUsize, Ordering};
const MAX_ITEMS: usize = 128;
static GLOBAL_MENUITEM_ID: AtomicUsize = AtomicUsize::new(0);
pub struct Menu {
pub(super) items: Vec<MenuItemWrapper>,
pub(super) current: VectorIndex,
pub(super) width: u16,
pub(super) text_width: u16,
pub(super) first_visible_item: u32,
pub(super) visible_items_count: u32,
pub(super) button_up: MenuButtonState,
pub(super) button_down: MenuButtonState,
pub(super) clip: ClipArea,
pub(super) handle: Handle<Menu>,
pub(super) parent_handle: Handle<Menu>,
pub(super) receiver_control_handle: Handle<()>,
}
impl Menu {
pub fn new() -> Self {
Self {
items: Vec::with_capacity(4),
current: VectorIndex::Invalid,
width: 1,
text_width: 0,
first_visible_item: 0,
visible_items_count: 0,
button_up: MenuButtonState::Normal,
button_down: MenuButtonState::Normal,
clip: ClipArea::new(0, 0, 1, 1),
handle: Handle::None,
parent_handle: Handle::None,
receiver_control_handle: Handle::None,
}
}
#[allow(private_bounds)]
pub fn add<T>(&mut self, mut menuitem: T) -> Handle<T>
where
T: MenuItem,
{
let id = (GLOBAL_MENUITEM_ID.fetch_add(1, Ordering::SeqCst) as u32) % 0xFFFF_FFFE;
let h: Handle<T> = Handle::with_id(id, self.items.len() as u32);
menuitem.update_handles(self.handle, h.cast());
self.items.push(menuitem.into_menuitem());
h
}
#[allow(private_bounds)]
pub fn get<T>(&self, menuitem_hamdle: Handle<T>) -> Option<&T>
where
T: MenuItem,
{
let idx = menuitem_hamdle.index();
if idx >= self.items.len() {
return None;
}
let item = &self.items[idx];
if item.get_handle() != menuitem_hamdle {
return None;
}
match item {
MenuItemWrapper::Command(obj) => Some(unsafe { &(*((obj as *const Command) as *const T)) }),
MenuItemWrapper::CheckBox(obj) => Some(unsafe { &(*((obj as *const CheckBox) as *const T)) }),
MenuItemWrapper::SingleChoice(obj) => Some(unsafe { &(*((obj as *const SingleChoice) as *const T)) }),
MenuItemWrapper::Separator(obj) => Some(unsafe { &(*((obj as *const Separator) as *const T)) }),
MenuItemWrapper::SubMenu(obj) => Some(unsafe { &(*((obj as *const SubMenu) as *const T)) }),
}
}
#[allow(private_bounds)]
pub fn get_mut<T>(&mut self, menuitem_hamdle: Handle<T>) -> Option<&mut T>
where
T: MenuItem,
{
let idx = menuitem_hamdle.index();
if idx >= self.items.len() {
return None;
}
let item = &mut self.items[idx];
if item.get_handle() != menuitem_hamdle {
return None;
}
match item {
MenuItemWrapper::Command(obj) => Some(unsafe { &mut (*((obj as *mut Command) as *mut T)) }),
MenuItemWrapper::CheckBox(obj) => Some(unsafe { &mut (*((obj as *mut CheckBox) as *mut T)) }),
MenuItemWrapper::SingleChoice(obj) => Some(unsafe { &mut (*((obj as *mut SingleChoice) as *mut T)) }),
MenuItemWrapper::Separator(obj) => Some(unsafe { &mut (*((obj as *mut Separator) as *mut T)) }),
MenuItemWrapper::SubMenu(obj) => Some(unsafe { &mut (*((obj as *mut SubMenu) as *mut T)) }),
}
}
pub(crate) fn is_on_menu(&self, x: i32, y: i32) -> bool {
MousePositionInfo::new(x - self.clip.left, y - self.clip.top, self).is_on_menu
}
#[inline(always)]
pub(crate) fn set_receiver_control_handle(&mut self, handle: Handle<()>) {
self.receiver_control_handle = handle;
}
pub(crate) fn update_menuitems_menu_handle(&mut self) {
for item in self.items.iter_mut() {
item.update_menu_handle(self.handle);
}
}
fn update_first_visible_item(&mut self) {
if !self.current.in_range(self.items.len()) {
return;
}
let cpoz = self.current.index() as u32;
self.first_visible_item = self.first_visible_item.min(cpoz);
if (cpoz - self.first_visible_item) > self.visible_items_count {
self.first_visible_item = (cpoz + 1) - self.visible_items_count;
}
}
fn move_currentitem_to(&mut self, key: Key) {
let mut idx: [usize; MAX_ITEMS] = [0usize; MAX_ITEMS];
let mut idx_count = 0usize;
let items_count = self.items.len();
for i in 0usize..items_count {
let item = &self.items[i];
if !item.is_enabled() {
continue;
}
if item.can_be_selected() {
idx[idx_count] = i;
idx_count += 1;
}
if idx_count >= items_count {
break;
}
}
if idx_count == 0 {
self.current = VectorIndex::Invalid;
return;
}
if !self.current.in_range(items_count) {
self.current.set(idx[0], self.items.len(), false);
} else {
let mut current_idx = VectorIndex::Invalid;
let mut best_diff = usize::MAX;
for (index, item) in idx.iter().enumerate().take(idx_count) {
let val = *item;
let diff = if val < self.current.index() {
self.current.index() - val
} else {
val - self.current.index()
};
if diff < best_diff {
best_diff = diff;
current_idx = VectorIndex::with_value(index);
}
}
if !current_idx.in_range(idx_count) {
self.current = VectorIndex::Invalid;
return;
}
match key.code {
KeyCode::Up => {
current_idx.sub(1, idx_count, Strategy::Rotate);
}
KeyCode::Down => {
current_idx.add(1, idx_count, Strategy::Rotate);
}
KeyCode::PageUp => {
current_idx.sub(self.visible_items_count as usize, idx_count, Strategy::Clamp);
}
KeyCode::PageDown => {
current_idx.add(self.visible_items_count as usize, idx_count, Strategy::Clamp);
}
KeyCode::Home => current_idx = VectorIndex::First,
KeyCode::End => current_idx = VectorIndex::last(idx_count),
_ => {}
}
self.current.set(idx[current_idx.index()], self.items.len(), false);
}
self.update_first_visible_item();
}
pub(in super::super) fn process_shortcut(&mut self, key: Key, receiver_control_handle: Handle<()>) -> bool {
for (index, item) in self.items.iter_mut().enumerate() {
if !item.is_enabled() {
continue;
}
if let Some(shortcut) = item.shortcut() {
if shortcut == key {
self.run_item_action(index, receiver_control_handle);
return true;
}
}
if let Some(submenu_handle) = item.get_submenu() {
if let Some(menu) = RuntimeManager::get().get_menu(submenu_handle) {
if menu.process_shortcut(key, receiver_control_handle) {
return true;
}
}
}
}
false
}
pub(crate) fn paint(&self, surface: &mut Surface, theme: &Theme, active: bool) {
let col = if active { &theme.menu } else { &theme.parent_menu };
surface.set_clip(self.clip.left, self.clip.top, self.clip.right, self.clip.bottom);
surface.set_origin(self.clip.left, self.clip.top);
surface.clear(Character::with_attributes(' ', col.text.normal));
surface.draw_rect(
Rect::new(0, 0, self.clip.right - self.clip.left, self.clip.bottom - self.clip.top),
LineType::Single,
col.text.normal,
);
if (self.visible_items_count as usize) < self.items.len() {
let c = self.button_up.get_color(self.first_visible_item == 0, col);
let x = (self.width >> 1) as i32;
surface.fill_horizontal_line(x, 0, x + 2, Character::with_attributes(' ', c));
surface.write_char(x + 1, 0, Character::with_attributes(SpecialChar::TriangleUp, c));
let c = self
.button_down
.get_color((self.first_visible_item + self.visible_items_count) as usize >= self.items.len(), col);
let y = self.clip.bottom - self.clip.top;
surface.fill_horizontal_line(x, y, x + 2, Character::with_attributes(' ', c));
surface.write_char(x + 1, y, Character::with_attributes(SpecialChar::TriangleDown, c));
}
let mut format = TextFormatBuilder::new()
.wrap_type(WrapType::SingleLineWrap(self.text_width))
.align(TextAlignment::Left)
.build();
let start = self.first_visible_item as usize;
let end = self.items.len().min((self.first_visible_item + self.visible_items_count) as usize);
if end <= start {
return;
}
for idx in start..end {
let item = &self.items[idx];
format.y += 1;
item.paint(surface, &mut format, self.width, idx == self.current.index(), col);
}
}
pub(crate) fn update_children_with_parent_handle(&self) {
let menus = RuntimeManager::get().get_menus();
for item in self.items.iter() {
if let Some(child_handle) = item.get_submenu() {
if let Some(menu) = menus.get_mut(child_handle) {
menu.parent_handle = self.handle;
}
}
}
}
pub(crate) fn on_mouse_released(&mut self, x: i32, y: i32) -> EventProcessStatus {
let x = x - self.clip.left;
let y = y - self.clip.top;
let mpi = MousePositionInfo::new(x, y, self);
if (self.visible_items_count as usize) < self.items.len() {
if (mpi.is_on_up_button) && (self.first_visible_item > 0) {
self.button_up = MenuButtonState::Hovered;
return EventProcessStatus::Processed;
}
if (mpi.is_on_down_button) && ((self.visible_items_count + self.first_visible_item) as usize) < self.items.len() {
self.button_down = MenuButtonState::Hovered;
return EventProcessStatus::Processed;
}
}
EventProcessStatus::Ignored
}
pub(crate) fn on_mouse_move(&mut self, x: i32, y: i32) -> MouseMoveMenuResult {
let x = x - self.clip.left;
let y = y - self.clip.top;
let mpi = MousePositionInfo::new(x, y, self);
let button_up_status = if mpi.is_on_up_button {
MenuButtonState::Hovered
} else {
MenuButtonState::Normal
};
let button_down_status = if mpi.is_on_down_button {
MenuButtonState::Hovered
} else {
MenuButtonState::Normal
};
let mut need_repaint = false;
if button_up_status != self.button_up {
self.button_up = button_up_status;
need_repaint = true;
}
if button_down_status != self.button_down {
self.button_down = button_down_status;
need_repaint = true;
}
if self.current != mpi.item_index {
self.current = mpi.item_index;
need_repaint = true;
}
if need_repaint {
if mpi.is_on_menu {
MouseMoveMenuResult::ProcessedAndRepaint
} else {
MouseMoveMenuResult::RepaintAndPass
}
} else if mpi.is_on_menu {
MouseMoveMenuResult::ProcessWithoutRepaint
} else {
MouseMoveMenuResult::Ignored
}
}
pub(crate) fn on_mouse_wheel(&mut self, direction: MouseWheelDirection) -> EventProcessStatus {
if (self.visible_items_count as usize) >= self.items.len() {
return EventProcessStatus::Ignored;
}
if (direction == MouseWheelDirection::Up) && (self.first_visible_item > 0) {
self.first_visible_item -= 1;
return EventProcessStatus::Processed;
}
if (direction == MouseWheelDirection::Down) && (((self.visible_items_count + self.first_visible_item) as usize) < self.items.len()) {
self.first_visible_item += 1;
return EventProcessStatus::Processed;
}
EventProcessStatus::Ignored
}
pub(crate) fn on_mouse_pressed(&mut self, x: i32, y: i32) -> MousePressedMenuResult {
let x = x - self.clip.left;
let y = y - self.clip.top;
let mpi = MousePositionInfo::new(x, y, self);
if (self.visible_items_count as usize) < self.items.len() {
if (mpi.is_on_up_button) && (self.first_visible_item > 0) {
self.button_up = MenuButtonState::Pressed;
self.on_mouse_wheel(MouseWheelDirection::Up);
return MousePressedMenuResult::Repaint;
}
if (mpi.is_on_down_button) && ((self.visible_items_count + self.first_visible_item) as usize) < self.items.len() {
self.button_down = MenuButtonState::Pressed;
self.on_mouse_wheel(MouseWheelDirection::Down);
return MousePressedMenuResult::Repaint;
}
}
if mpi.item_index.is_valid() {
self.run_item_action(mpi.item_index.index(), self.receiver_control_handle);
return MousePressedMenuResult::Repaint;
}
if mpi.is_on_menu {
return MousePressedMenuResult::None;
}
MousePressedMenuResult::CheckParent
}
pub(super) fn select_single_choice(&mut self, index: usize) {
let count = self.items.len();
if index >= count {
return;
}
if !self.items[index].is_singlechoice() {
return;
}
let mut idx = index;
while (idx > 0) && (self.items[idx].is_singlechoice()) {
self.items[idx].set_checked(false);
idx -= 1;
}
if (idx == 0) && (self.items[0].is_singlechoice()) {
self.items[0].set_checked(false);
}
idx = index;
while (idx < count) && (self.items[idx].is_singlechoice()) {
self.items[idx].set_checked(false);
idx += 1;
}
self.items[index].set_checked(true);
}
fn send_event(&mut self, event: MenuEvent) {
let rm = RuntimeManager::get();
rm.close_opened_menu();
rm.set_menu_event(event);
}
fn close(&mut self) {
RuntimeManager::get().activate_opened_menu_parent();
}
fn run_item_action(&mut self, index: usize, receiver_control_handle: Handle<()>) {
if index >= self.items.len() {
return;
}
if !self.items[index].is_enabled() {
return;
}
match &mut self.items[index] {
MenuItemWrapper::Command(item) => {
let evnt = MenuEvent::Command(MenuCommandEvent {
command_id: item.command_id,
menu: self.handle,
item: item.handle,
control_receiver_handle: receiver_control_handle,
});
self.send_event(evnt);
}
MenuItemWrapper::CheckBox(item) => {
item.checked = !item.checked;
let evnt = MenuEvent::CheckBoxStateChanged(MenuCheckBoxStateChangedEvent {
command_id: item.command_id,
menu: self.handle,
item: item.handle,
checked: item.checked,
control_receiver_handle: receiver_control_handle,
});
self.send_event(evnt);
}
MenuItemWrapper::SingleChoice(item) => {
let evnt = MenuEvent::SingleChoiceSelected(MenuRadioBoxSelectedEvent {
command_id: item.command_id,
menu: self.handle,
item: item.handle,
control_receiver_handle: receiver_control_handle,
});
self.select_single_choice(index);
self.send_event(evnt);
}
MenuItemWrapper::Separator(_) => {}
MenuItemWrapper::SubMenu(item) => {
RuntimeManager::get().show_menu(
item.submenu_handle,
receiver_control_handle,
(self.width as i32) + self.clip.left,
self.clip.top + 1 + ((index as u32 - self.first_visible_item) as i32),
0,
None,
);
}
};
}
pub(crate) fn on_key_pressed(&mut self, key: Key) -> EventProcessStatus {
match key.value() {
key!("Up") | key!("Down") | key!("Home") | key!("End") | key!("PageUp") | key!("PageDown") => {
self.move_currentitem_to(key);
return EventProcessStatus::Processed;
}
key!("Enter") | key!("Space") => {
self.run_item_action(self.current.index(), self.receiver_control_handle);
return EventProcessStatus::Processed;
}
key!("Escape") => {
self.close();
return EventProcessStatus::Processed;
}
key!("Left") => {
if self.parent_handle.is_none() {
return EventProcessStatus::Ignored;
}
RuntimeManager::get().activate_opened_menu_parent();
return EventProcessStatus::Processed;
}
key!("Right") => {
if self.current.in_range(self.items.len()) {
let item = &self.items[self.current.index()];
if (item.is_enabled()) && (item.is_submenu()) {
self.run_item_action(self.current.index(), self.receiver_control_handle);
return EventProcessStatus::Processed;
}
}
return EventProcessStatus::Ignored;
}
_ => {}
}
if (key.code != KeyCode::None) && (key.modifier == KeyModifier::None) {
let count = self.items.len();
let mut idx = 0usize;
while idx < count {
let item = &self.items[idx];
if item.is_enabled() {
if let Some(hotkey) = item.hotkey() {
if hotkey == key {
self.current = VectorIndex::with_value(idx);
self.update_first_visible_item();
self.run_item_action(idx, self.receiver_control_handle);
return EventProcessStatus::Processed;
}
}
}
idx += 1;
}
}
EventProcessStatus::Ignored
}
pub(crate) fn compute_position(&mut self, x: i32, y: i32, title_width: u32, max_size: Size, term_size: Size) -> bool {
if (term_size.width < 5) || (term_size.height < 5) {
return false;
}
let mut max_width_left = 0usize;
let mut max_hot_key_width = 0usize;
for item in &self.items {
let mut w_left = item.get_caption_chars_count() + 4;
let mut w_right = 0usize;
if item.is_checkable() {
w_left += 2;
}
if let Some(shortcut) = item.shortcut() {
w_right += shortcut.code.name().len();
w_right += shortcut.modifier.name().len();
if w_right > 0 {
w_right += 2;
}
}
max_width_left = max_width_left.max(w_left);
max_hot_key_width = max_hot_key_width.max(w_right);
}
let best_width = (max_width_left + max_hot_key_width) | 1;
let mut x = x.clamp(0, term_size.width as i32);
let y = y.clamp(0, term_size.height as i32);
let mut max_width_for_current_screen = 37u32.max(term_size.width / 4); let mut max_height_for_current_screen = 5u32.max(term_size.height - 4);
if max_size.width >= 30 {
max_width_for_current_screen = max_width_for_current_screen.min(max_size.width | 1);
}
if max_size.height >= 5 {
max_height_for_current_screen = max_height_for_current_screen.min(max_size.height);
}
let menu_width = max_width_for_current_screen.min((best_width as u32) + 2);
let mut menu_height = max_height_for_current_screen.min((self.items.len() as u32) + 2);
let to_left = {
if x + (menu_width as i32) <= (term_size.width as i32) {
true } else if x >= (menu_width as i32) {
false } else {
x < ((term_size.width / 2) as i32) }
};
if (!to_left) && (title_width > 1) {
x = ((x + (title_width as i32)).min(term_size.width as i32)) - 1;
}
let to_bottom = {
if y + (menu_height as i32) <= (term_size.height as i32) {
true } else if y >= (menu_height as i32) {
false } else {
let result = y < ((term_size.height / 2) as i32); if result {
menu_height = 5u32.max(((term_size.height as i32) - y) as u32);
} else {
menu_height = 5u32.max(y as u32); }
result
}
};
self.visible_items_count = menu_height - 2;
self.width = (menu_width - 2) as u16;
self.text_width = self.width.saturating_sub((max_hot_key_width + 2) as u16);
if to_left {
if to_bottom {
self.clip.set_with_size(x, y, menu_width as u16, menu_height as u16);
} else {
self.clip
.set_with_size(x, y + 1 - (menu_height as i32), menu_width as u16, menu_height as u16);
}
} else if to_bottom {
self.clip
.set_with_size(x + 1 - (menu_width as i32), y, menu_width as u16, menu_height as u16);
} else {
self.clip.set_with_size(
x + 1 - (menu_width as i32),
y + 1 - (menu_height as i32),
menu_width as u16,
menu_height as u16,
);
}
self.first_visible_item = 0;
self.current = VectorIndex::Invalid;
self.button_up = MenuButtonState::Normal;
self.button_down = MenuButtonState::Normal;
true
}
#[inline(always)]
pub(crate) fn get_parent_handle(&self) -> Handle<Menu> {
self.parent_handle
}
}
impl HandleSupport<Menu> for Menu {
fn handle(&self) -> Handle<Menu> {
self.handle.cast()
}
fn set_handle(&mut self, handle: Handle<Menu>) {
self.handle = handle;
self.update_children_with_parent_handle();
}
}