use nalgebra_glm::{Vec2, Vec4};
use crate::ecs::text::components::{TextAlignment, VerticalAlignment};
use crate::ecs::ui::builder::UiTreeBuilder;
use crate::ecs::ui::components::*;
use crate::ecs::ui::layout_types::{FlowAlignment, FlowDirection};
use crate::ecs::ui::state::{UiBase, UiHover};
use crate::ecs::ui::types::Anchor;
use crate::ecs::ui::units::{Ab, Rl};
use crate::render::wgpu::passes::geometry::UiLayer;
use super::{ContextMenuBuilder, ContextMenuBuilderEntry, ContextMenuTheme};
impl<'a> UiTreeBuilder<'a> {
pub fn add_context_menu(&mut self, items: &[(&str, Option<&str>)]) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let corner_radius = theme.corner_radius;
let border_color = theme.border_color;
let button_height = theme.button_height;
let mut item_entities = Vec::new();
let context_items: Vec<ContextMenuItem> = items
.iter()
.map(|(label, shortcut)| ContextMenuItem {
label: label.to_string(),
shortcut: shortcut.map(|s| s.to_string()),
separator: label.is_empty(),
})
.collect();
let popup_height = items.iter().fold(0.0f32, |acc, (label, _)| {
if label.is_empty() {
acc + 5.0
} else {
acc + button_height
}
});
let popup_entity = self
.add_node()
.window(
Ab(Vec2::new(0.0, 0.0)),
Ab(Vec2::new(200.0, popup_height)),
Anchor::TopLeft,
)
.with_rect(corner_radius, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_layer(UiLayer::Popups)
.with_depth(UiDepthMode::Set(35.0))
.with_visible(false)
.flow(FlowDirection::Vertical, 2.0, 0.0)
.entity();
self.push_parent(popup_entity);
for (label, shortcut) in items {
if label.is_empty() {
self.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 1.0)))
.with_rect(0.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Border)
.without_pointer_events()
.done();
item_entities.push(freecs::Entity::default());
continue;
}
let label_slot = self.world_mut().resources.text_cache.add_text(*label);
let item_entity = if let Some(sc) = shortcut {
let sc_slot = self.world_mut().resources.text_cache.add_text(*sc);
self.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.flow(FlowDirection::Horizontal, 8.0, 0.0)
.with_children(|tree| {
tree.add_node()
.flow_child(Rl(Vec2::new(0.0, 100.0)))
.flex_grow(1.0)
.with_text_slot(label_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
tree.add_node()
.flow_child(Rl(Vec2::new(0.0, 100.0)))
.auto_size(crate::ecs::ui::components::AutoSizeMode::Width)
.auto_size_padding(Vec2::new(4.0, 0.0))
.with_text_slot(sc_slot, font_size * 0.85)
.with_text_alignment(TextAlignment::Right, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::TextDisabled)
.without_pointer_events()
.done();
})
.done()
} else {
self.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
tree.add_node()
.boundary(Ab(Vec2::new(8.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
.with_text_slot(label_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done()
};
if let Some(interaction) = self.world_mut().ui.get_ui_node_interaction_mut(item_entity)
{
interaction.accessible_role = Some(AccessibleRole::MenuItem);
}
item_entities.push(item_entity);
}
self.pop_parent();
let menu_entity = popup_entity;
self.world_mut()
.ui
.set_ui_node_interaction(menu_entity, UiNodeInteraction::default());
if let Some(interaction) = self.world_mut().ui.get_ui_node_interaction_mut(menu_entity) {
interaction.accessible_role = Some(AccessibleRole::Menu);
}
self.world_mut().ui.set_ui_widget_state(
menu_entity,
UiWidgetState::ContextMenu(UiContextMenuData {
items: context_items,
open: false,
clicked_item: None,
popup_entity,
item_entities,
item_defs: Vec::new(),
}),
);
menu_entity
}
pub fn add_context_menu_from_builder(&mut self, builder: ContextMenuBuilder) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let corner_radius = theme.corner_radius;
let button_height = theme.button_height;
let mut command_counter = 0usize;
let cm_theme = ContextMenuTheme {
font_size,
corner_radius,
button_height,
};
let popup_entity =
self.build_context_menu_popup(&builder.entries, &mut command_counter, &cm_theme, 0);
let item_defs = if let Some(UiWidgetState::ContextMenu(data)) =
self.world_mut().ui.get_ui_widget_state(popup_entity)
{
data.item_defs.clone()
} else {
Vec::new()
};
let item_entities: Vec<freecs::Entity> =
item_defs.iter().map(|def| def.row_entity).collect();
self.world_mut()
.ui
.set_ui_node_interaction(popup_entity, UiNodeInteraction::default());
if let Some(interaction) = self
.world_mut()
.ui
.get_ui_node_interaction_mut(popup_entity)
{
interaction.accessible_role = Some(AccessibleRole::Menu);
}
self.world_mut().ui.set_ui_widget_state(
popup_entity,
UiWidgetState::ContextMenu(UiContextMenuData {
items: Vec::new(),
open: false,
clicked_item: None,
popup_entity,
item_entities,
item_defs,
}),
);
popup_entity
}
fn build_context_menu_popup(
&mut self,
entries: &[ContextMenuBuilderEntry],
command_counter: &mut usize,
theme: &ContextMenuTheme,
depth_level: u32,
) -> freecs::Entity {
let font_size = theme.font_size;
let corner_radius = theme.corner_radius;
let button_height = theme.button_height;
let border_color = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme()
.border_color;
let popup_height = entries.iter().fold(0.0f32, |acc, entry| match entry {
ContextMenuBuilderEntry::Separator => acc + 5.0,
_ => acc + button_height,
});
let popup_entity = self
.add_node()
.window(
Ab(Vec2::new(0.0, 0.0)),
Ab(Vec2::new(200.0, popup_height)),
Anchor::TopLeft,
)
.with_rect(corner_radius, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_layer(UiLayer::Popups)
.with_depth(UiDepthMode::Set(36.0 + depth_level as f32))
.with_visible(false)
.flow(FlowDirection::Vertical, 2.0, 0.0)
.entity();
self.push_parent(popup_entity);
let mut item_defs = Vec::new();
for entry in entries {
match entry {
ContextMenuBuilderEntry::Separator => {
self.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 1.0)))
.with_rect(0.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Border)
.without_pointer_events()
.done();
item_defs.push(ContextMenuItemDef {
label: String::new(),
shortcut: String::new(),
kind: ContextMenuItemKind::Separator,
row_entity: freecs::Entity::default(),
command_id: None,
binding: None,
});
}
ContextMenuBuilderEntry::Item { label, shortcut } => {
let cmd_id = *command_counter;
*command_counter += 1;
let label_slot = self.world_mut().resources.text_cache.add_text(label);
let row_entity = if shortcut.is_empty() {
self.add_node()
.flow_child(
Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)),
)
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
tree.add_node()
.boundary(Ab(Vec2::new(8.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
.with_text_slot(label_slot, font_size)
.with_text_alignment(
TextAlignment::Left,
VerticalAlignment::Middle,
)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done()
} else {
let sc_slot = self.world_mut().resources.text_cache.add_text(shortcut);
self.add_node()
.flow_child(
Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)),
)
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.flow(FlowDirection::Horizontal, 8.0, 0.0)
.with_children(|tree| {
tree.add_node()
.flow_child(Rl(Vec2::new(0.0, 100.0)))
.flex_grow(1.0)
.with_text_slot(label_slot, font_size)
.with_text_alignment(
TextAlignment::Left,
VerticalAlignment::Middle,
)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
tree.add_node()
.flow_child(Rl(Vec2::new(0.0, 100.0)))
.auto_size(crate::ecs::ui::components::AutoSizeMode::Width)
.auto_size_padding(Vec2::new(4.0, 0.0))
.with_text_slot(sc_slot, font_size * 0.85)
.with_text_alignment(
TextAlignment::Right,
VerticalAlignment::Middle,
)
.with_theme_color::<UiBase>(ThemeColor::TextDisabled)
.without_pointer_events()
.done();
})
.done()
};
item_defs.push(ContextMenuItemDef {
label: label.clone(),
shortcut: shortcut.clone(),
kind: ContextMenuItemKind::Action,
row_entity,
command_id: Some(cmd_id),
binding: ShortcutBinding::parse(shortcut),
});
}
ContextMenuBuilderEntry::Submenu { label, children } => {
let label_slot = self.world_mut().resources.text_cache.add_text(label);
let arrow_slot = self.world_mut().resources.text_cache.add_text("\u{25B6}");
let row_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.flow(FlowDirection::Horizontal, 8.0, 0.0)
.with_children(|tree| {
tree.add_node()
.flow_child(Rl(Vec2::new(0.0, 100.0)))
.flex_grow(1.0)
.with_text_slot(label_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
tree.add_node()
.flow_child(Ab(Vec2::new(16.0, 0.0)) + Rl(Vec2::new(0.0, 100.0)))
.with_text_slot(arrow_slot, font_size * 0.85)
.with_text_alignment(
TextAlignment::Center,
VerticalAlignment::Middle,
)
.with_theme_color::<UiBase>(ThemeColor::TextDisabled)
.without_pointer_events()
.done();
})
.done();
self.pop_parent();
let child_popup = self.build_context_menu_popup(
children,
command_counter,
theme,
depth_level + 1,
);
self.push_parent(popup_entity);
let child_defs = if let Some(UiWidgetState::ContextMenu(data)) =
self.world_mut().ui.get_ui_widget_state(child_popup)
{
data.item_defs.clone()
} else {
Vec::new()
};
item_defs.push(ContextMenuItemDef {
label: label.clone(),
shortcut: String::new(),
kind: ContextMenuItemKind::Submenu {
children: child_defs,
popup_entity: child_popup,
open: false,
},
row_entity,
command_id: None,
binding: None,
});
}
ContextMenuBuilderEntry::WidgetRow { label } => {
let cmd_id = *command_counter;
*command_counter += 1;
let label_slot = self.world_mut().resources.text_cache.add_text(label);
let mut content_entity = freecs::Entity::default();
let row_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_interaction()
.flow_with_alignment(
FlowDirection::Horizontal,
8.0,
0.0,
FlowAlignment::Start,
FlowAlignment::Center,
)
.with_children(|tree| {
tree.add_node()
.flow_child(Ab(Vec2::new(0.0, button_height)))
.flex_grow(1.0)
.with_text_slot(label_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
content_entity = tree
.add_node()
.flow_child(Ab(Vec2::new(0.0, button_height)))
.flow_with_alignment(
FlowDirection::Horizontal,
0.0,
4.0,
FlowAlignment::End,
FlowAlignment::Center,
)
.auto_size(crate::ecs::ui::components::AutoSizeMode::Width)
.entity();
})
.done();
item_defs.push(ContextMenuItemDef {
label: label.clone(),
shortcut: String::new(),
kind: ContextMenuItemKind::Widget { content_entity },
row_entity,
command_id: Some(cmd_id),
binding: None,
});
}
}
}
self.pop_parent();
self.world_mut().ui.set_ui_widget_state(
popup_entity,
UiWidgetState::ContextMenu(UiContextMenuData {
items: Vec::new(),
open: false,
clicked_item: None,
popup_entity,
item_entities: item_defs.iter().map(|d| d.row_entity).collect(),
item_defs,
}),
);
popup_entity
}
}