use std::collections::HashMap;
use nalgebra_glm::{Vec2, Vec4};
use crate::ecs::ui::components::{DragPayload, TileId};
use crate::ecs::ui::composite::AnyCompositeWidget;
use crate::ecs::text::components::{TextMesh, TextProperties};
#[derive(Clone, Debug)]
pub struct ActiveDrag {
pub source: freecs::Entity,
pub payload: DragPayload,
pub start_pos: Vec2,
pub current_pos: Vec2,
}
use crate::ecs::ui::theme::ThemeState;
use crate::ecs::ui::types::{Rect, UiTextInstance};
use crate::render::wgpu::passes::geometry::{UiImage, UiLayer, UiRect};
pub struct SecondaryWindowUiBuffers {
pub z_sorted_nodes: Vec<(freecs::Entity, f32)>,
pub frame_rects: Vec<UiRect>,
pub frame_ui_images: Vec<UiImage>,
pub frame_text_meshes: Vec<UiTextInstance>,
}
#[derive(Clone)]
pub struct CachedTextMesh {
pub mesh: TextMesh,
pub slot_generation: u64,
pub font_size: f32,
pub wrap_width: Option<f32>,
pub monospace_width: Option<f32>,
}
#[derive(Clone, Debug)]
pub enum UiEvent {
ButtonClicked(freecs::Entity),
SliderChanged {
entity: freecs::Entity,
value: f32,
},
ToggleChanged {
entity: freecs::Entity,
value: bool,
},
CheckboxChanged {
entity: freecs::Entity,
value: bool,
},
RadioChanged {
entity: freecs::Entity,
group_id: u32,
option_index: usize,
},
TabChanged {
entity: freecs::Entity,
tab_index: usize,
},
TextInputChanged {
entity: freecs::Entity,
text: String,
},
TextInputSubmitted {
entity: freecs::Entity,
text: String,
},
DropdownChanged {
entity: freecs::Entity,
selected_index: usize,
},
MenuItemClicked {
entity: freecs::Entity,
item_index: usize,
},
ColorPickerChanged {
entity: freecs::Entity,
color: Vec4,
},
SelectableLabelClicked {
entity: freecs::Entity,
selected: bool,
},
DragValueChanged {
entity: freecs::Entity,
value: f32,
},
ContextMenuItemClicked {
entity: freecs::Entity,
item_index: usize,
},
TreeNodeSelected {
tree: freecs::Entity,
node: freecs::Entity,
selected: bool,
},
TreeNodeToggled {
tree: freecs::Entity,
node: freecs::Entity,
expanded: bool,
},
TreeNodeContextMenu {
tree: freecs::Entity,
node: freecs::Entity,
position: Vec2,
},
TreeNodeExpandRequested {
tree: freecs::Entity,
node: freecs::Entity,
user_data: u64,
},
ModalClosed {
entity: freecs::Entity,
confirmed: bool,
},
CommandPaletteExecuted {
entity: freecs::Entity,
command_index: usize,
},
TileTabActivated {
container: freecs::Entity,
pane_id: TileId,
},
TileTabClosed {
container: freecs::Entity,
pane_id: TileId,
title: String,
},
TileSplitterMoved {
container: freecs::Entity,
split_id: TileId,
ratio: f32,
},
DragStarted {
source: freecs::Entity,
payload: DragPayload,
},
DragDropped {
source: freecs::Entity,
target: freecs::Entity,
payload: DragPayload,
},
DragCancelled {
source: freecs::Entity,
},
DragEnter {
target: freecs::Entity,
source: freecs::Entity,
payload: DragPayload,
},
DragOver {
target: freecs::Entity,
source: freecs::Entity,
position: Vec2,
},
DragLeave {
target: freecs::Entity,
source: freecs::Entity,
},
ShortcutTriggered {
command_index: usize,
},
CanvasClicked {
entity: freecs::Entity,
command_id: u32,
position: Vec2,
},
VirtualListItemClicked {
entity: freecs::Entity,
item_index: usize,
},
TextAreaChanged {
entity: freecs::Entity,
text: String,
},
RichTextEditorChanged {
entity: freecs::Entity,
text: String,
},
DataGridFilterChanged {
entity: freecs::Entity,
filters: Vec<String>,
},
RangeSliderChanged {
entity: freecs::Entity,
low: f32,
high: f32,
},
DataGridCellEdited {
entity: freecs::Entity,
row: usize,
column: usize,
text: String,
},
BreadcrumbClicked {
entity: freecs::Entity,
segment_index: usize,
},
SplitterChanged {
entity: freecs::Entity,
ratio: f32,
},
MultiSelectChanged {
entity: freecs::Entity,
selected_indices: Vec<usize>,
},
DatePickerChanged {
entity: freecs::Entity,
year: i32,
month: u32,
day: u32,
},
}
impl UiEvent {
pub fn target_entity(&self) -> freecs::Entity {
match self {
UiEvent::ButtonClicked(entity) => *entity,
UiEvent::SliderChanged { entity, .. } => *entity,
UiEvent::ToggleChanged { entity, .. } => *entity,
UiEvent::CheckboxChanged { entity, .. } => *entity,
UiEvent::RadioChanged { entity, .. } => *entity,
UiEvent::TabChanged { entity, .. } => *entity,
UiEvent::TextInputChanged { entity, .. } => *entity,
UiEvent::TextInputSubmitted { entity, .. } => *entity,
UiEvent::DropdownChanged { entity, .. } => *entity,
UiEvent::MenuItemClicked { entity, .. } => *entity,
UiEvent::ColorPickerChanged { entity, .. } => *entity,
UiEvent::SelectableLabelClicked { entity, .. } => *entity,
UiEvent::DragValueChanged { entity, .. } => *entity,
UiEvent::ContextMenuItemClicked { entity, .. } => *entity,
UiEvent::TreeNodeSelected { node, .. } => *node,
UiEvent::TreeNodeToggled { node, .. } => *node,
UiEvent::TreeNodeContextMenu { node, .. } => *node,
UiEvent::TreeNodeExpandRequested { node, .. } => *node,
UiEvent::ModalClosed { entity, .. } => *entity,
UiEvent::CommandPaletteExecuted { entity, .. } => *entity,
UiEvent::TileTabActivated { container, .. } => *container,
UiEvent::TileTabClosed { container, .. } => *container,
UiEvent::TileSplitterMoved { container, .. } => *container,
UiEvent::DragStarted { source, .. } => *source,
UiEvent::DragDropped { target, .. } => *target,
UiEvent::DragCancelled { source, .. } => *source,
UiEvent::DragEnter { target, .. } => *target,
UiEvent::DragOver { target, .. } => *target,
UiEvent::DragLeave { target, .. } => *target,
UiEvent::ShortcutTriggered { .. } => freecs::Entity::default(),
UiEvent::CanvasClicked { entity, .. } => *entity,
UiEvent::VirtualListItemClicked { entity, .. } => *entity,
UiEvent::TextAreaChanged { entity, .. } => *entity,
UiEvent::RichTextEditorChanged { entity, .. } => *entity,
UiEvent::DataGridFilterChanged { entity, .. } => *entity,
UiEvent::RangeSliderChanged { entity, .. } => *entity,
UiEvent::DataGridCellEdited { entity, .. } => *entity,
UiEvent::BreadcrumbClicked { entity, .. } => *entity,
UiEvent::SplitterChanged { entity, .. } => *entity,
UiEvent::MultiSelectChanged { entity, .. } => *entity,
UiEvent::DatePickerChanged { entity, .. } => *entity,
}
}
pub fn bubbles(&self) -> bool {
match self {
UiEvent::ButtonClicked(_)
| UiEvent::SliderChanged { .. }
| UiEvent::ToggleChanged { .. }
| UiEvent::CheckboxChanged { .. }
| UiEvent::RadioChanged { .. }
| UiEvent::TabChanged { .. }
| UiEvent::TextInputChanged { .. }
| UiEvent::TextInputSubmitted { .. }
| UiEvent::DropdownChanged { .. }
| UiEvent::MenuItemClicked { .. }
| UiEvent::ColorPickerChanged { .. }
| UiEvent::SelectableLabelClicked { .. }
| UiEvent::DragValueChanged { .. }
| UiEvent::ContextMenuItemClicked { .. }
| UiEvent::TreeNodeSelected { .. }
| UiEvent::TreeNodeToggled { .. } => true,
UiEvent::TreeNodeContextMenu { .. }
| UiEvent::TreeNodeExpandRequested { .. }
| UiEvent::ModalClosed { .. }
| UiEvent::CommandPaletteExecuted { .. }
| UiEvent::TileTabActivated { .. }
| UiEvent::TileTabClosed { .. }
| UiEvent::TileSplitterMoved { .. }
| UiEvent::DragStarted { .. }
| UiEvent::DragDropped { .. }
| UiEvent::DragCancelled { .. }
| UiEvent::DragEnter { .. }
| UiEvent::DragOver { .. }
| UiEvent::DragLeave { .. }
| UiEvent::ShortcutTriggered { .. }
| UiEvent::CanvasClicked { .. }
| UiEvent::VirtualListItemClicked { .. }
| UiEvent::DataGridFilterChanged { .. }
| UiEvent::DataGridCellEdited { .. } => false,
UiEvent::TextAreaChanged { .. }
| UiEvent::RichTextEditorChanged { .. }
| UiEvent::RangeSliderChanged { .. }
| UiEvent::BreadcrumbClicked { .. }
| UiEvent::SplitterChanged { .. }
| UiEvent::MultiSelectChanged { .. }
| UiEvent::DatePickerChanged { .. } => true,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
pub struct PropertyId(pub u32);
#[derive(Clone)]
pub enum PropertyValue {
F32(f32),
Bool(bool),
Usize(usize),
String(String),
Vec4(Vec4),
}
pub trait FromPropertyValue: Sized {
fn from_property_value(value: &PropertyValue) -> Option<Self>;
fn default_value() -> Self;
}
pub trait IntoPropertyValue {
fn into_property_value(self) -> PropertyValue;
}
macro_rules! impl_property_value {
($variant:ident, $ty:ty, $default:expr, copy) => {
impl FromPropertyValue for $ty {
fn from_property_value(value: &PropertyValue) -> Option<Self> {
match value {
PropertyValue::$variant(v) => Some(*v),
_ => None,
}
}
fn default_value() -> Self {
$default
}
}
impl IntoPropertyValue for $ty {
fn into_property_value(self) -> PropertyValue {
PropertyValue::$variant(self)
}
}
};
($variant:ident, $ty:ty, $default:expr, clone) => {
impl FromPropertyValue for $ty {
fn from_property_value(value: &PropertyValue) -> Option<Self> {
match value {
PropertyValue::$variant(v) => Some(v.clone()),
_ => None,
}
}
fn default_value() -> Self {
$default
}
}
impl IntoPropertyValue for $ty {
fn into_property_value(self) -> PropertyValue {
PropertyValue::$variant(self)
}
}
};
}
impl_property_value!(F32, f32, 0.0, copy);
impl_property_value!(Bool, bool, false, copy);
impl_property_value!(Usize, usize, 0, copy);
impl_property_value!(String, String, String::new(), clone);
impl_property_value!(Vec4, Vec4, Vec4::new(0.0, 0.0, 0.0, 0.0), copy);
pub(crate) struct BoundProperty {
pub value: PropertyValue,
pub entity: freecs::Entity,
pub dirty_from_widget: bool,
pub dirty_from_code: bool,
}
pub type UiEventHandler = Box<dyn FnMut(&mut crate::ecs::world::World, &UiEvent)>;
pub type PropertyReaction = Box<dyn FnMut(&PropertyValue, &mut crate::ecs::world::World)>;
pub type ClickReaction = Box<dyn FnMut(&mut crate::ecs::world::World)>;
pub type SubmitReaction = Box<dyn FnMut(String, &mut crate::ecs::world::World)>;
pub type ConfirmReaction = Box<dyn FnMut(bool, &mut crate::ecs::world::World)>;
pub type MenuSelectReaction = Box<dyn FnMut(usize, &mut crate::ecs::world::World)>;
pub type CommandReaction = Box<dyn FnMut(usize, &mut crate::ecs::world::World)>;
pub type TreeSelectReaction = Box<dyn FnMut(freecs::Entity, &mut crate::ecs::world::World)>;
pub type TreeContextMenuReaction =
Box<dyn FnMut(freecs::Entity, Vec2, &mut crate::ecs::world::World)>;
pub type MultiSelectReaction = Box<dyn FnMut(Vec<usize>, &mut crate::ecs::world::World)>;
pub type DateChangedReaction = Box<dyn FnMut(i32, u32, u32, &mut crate::ecs::world::World)>;
pub type ChangedReaction = Box<dyn FnMut(&mut crate::ecs::world::World)>;
#[derive(Default)]
pub struct RetainedUiState {
pub enabled: bool,
pub theme_state: ThemeState,
pub focused_entity: Option<freecs::Entity>,
pub hovered_entity: Option<freecs::Entity>,
pub active_entity: Option<freecs::Entity>,
pub requested_cursor: Option<winit::window::CursorIcon>,
pub z_sorted_nodes: Vec<(freecs::Entity, f32)>,
pub frame_rects: Vec<UiRect>,
pub frame_ui_images: Vec<UiImage>,
pub frame_text_meshes: Vec<UiTextInstance>,
pub frame_chars: Vec<char>,
pub frame_keys: Vec<(winit::keyboard::KeyCode, bool)>,
pub ctrl_held: bool,
pub shift_held: bool,
pub scroll_delta: Vec2,
pub current_time: f64,
pub delta_time: f32,
pub last_click: Option<(freecs::Entity, f64, Vec2)>,
pub background_color: Option<Vec4>,
pub tooltip_state: Option<TooltipActiveState>,
pub popup_entities: Vec<freecs::Entity>,
pub reserved_areas: ReservedAreas,
pub next_focus_order: i32,
pub frame_events: Vec<UiEvent>,
pub focus_ring_visible: bool,
pub toast_container: Option<freecs::Entity>,
pub toast_entries: Vec<ToastEntry>,
pub text_mesh_cache: Vec<Option<CachedTextMesh>>,
pub text_mesh_cache_dpi: f32,
pub animation_states: HashMap<(freecs::Entity, u32), (f32, f64)>,
pub active_drag: Option<ActiveDrag>,
pub drag_over_entity: Option<freecs::Entity>,
pub shortcut_bindings: Vec<(crate::ecs::ui::components::ShortcutBinding, usize)>,
pub dock_indicator_active: bool,
pub dock_indicator_panel: Option<freecs::Entity>,
pub active_context_menu: Option<freecs::Entity>,
pub active_modal: Option<freecs::Entity>,
pub bubbled_events: Vec<BubbledUiEvent>,
pub(crate) composite_widgets: HashMap<freecs::Entity, Box<dyn AnyCompositeWidget>>,
pub overlay_rects: Vec<UiRect>,
pub overlay_text: Vec<OverlayText>,
pub last_compute_viewport: Option<(u32, u32)>,
pub last_compute_dpi: f32,
pub last_compute_text_generation: u64,
pub layout_dirty: bool,
pub render_dirty: bool,
pub cached_node_rect_count: usize,
pub cached_node_image_count: usize,
pub cached_node_text_count: usize,
pub secondary_buffers: HashMap<usize, SecondaryWindowUiBuffers>,
pub text_slot_character_colors: HashMap<usize, Vec<Option<Vec4>>>,
pub text_slot_character_background_colors: HashMap<usize, Vec<Option<Vec4>>>,
pub event_handlers: HashMap<freecs::Entity, Vec<UiEventHandler>>,
pub picking_grid: Option<crate::ecs::ui::picking::PickingGrid>,
pub custom_state_count: usize,
pub(crate) bound_properties: HashMap<PropertyId, BoundProperty>,
pub next_property_id: u32,
pub(crate) named_properties: HashMap<String, PropertyId>,
pub(crate) property_reactions: HashMap<String, Vec<PropertyReaction>>,
pub(crate) click_reactions: HashMap<freecs::Entity, Vec<ClickReaction>>,
pub(crate) submit_reactions: HashMap<freecs::Entity, Vec<SubmitReaction>>,
pub(crate) confirm_reactions: HashMap<freecs::Entity, Vec<ConfirmReaction>>,
pub(crate) menu_select_reactions: HashMap<freecs::Entity, Vec<MenuSelectReaction>>,
pub(crate) command_reactions: HashMap<freecs::Entity, Vec<CommandReaction>>,
pub(crate) tree_select_reactions: HashMap<freecs::Entity, Vec<TreeSelectReaction>>,
pub(crate) tree_context_menu_reactions: HashMap<freecs::Entity, Vec<TreeContextMenuReaction>>,
pub(crate) multi_select_reactions: HashMap<freecs::Entity, Vec<MultiSelectReaction>>,
pub(crate) date_changed_reactions: HashMap<freecs::Entity, Vec<DateChangedReaction>>,
pub(crate) changed_reactions: HashMap<freecs::Entity, Vec<ChangedReaction>>,
pub test_id_map: HashMap<String, freecs::Entity>,
pub subpixel_text_rendering: bool,
pub reduced_motion: bool,
pub announce_queue: Vec<String>,
pub clipboard_text: String,
pub radio_groups: HashMap<u32, Vec<freecs::Entity>>,
pub selectable_label_groups: HashMap<u32, Vec<freecs::Entity>>,
#[cfg(feature = "syntax_highlighting")]
pub syntax_highlighter: Option<crate::ecs::ui::syntax::SyntaxHighlighter>,
}
#[derive(Clone, Debug)]
pub struct BubbledUiEvent {
pub event: UiEvent,
pub target: freecs::Entity,
pub ancestor: freecs::Entity,
pub stopped: bool,
}
impl BubbledUiEvent {
pub fn stop_propagation(&mut self) {
self.stopped = true;
}
pub fn stopped(&self) -> bool {
self.stopped
}
}
#[derive(Clone, Debug)]
pub struct TooltipActiveState {
pub entity: freecs::Entity,
pub hover_start: f64,
pub text: String,
pub tooltip_entity: Option<freecs::Entity>,
}
#[derive(Clone, Debug, Default)]
pub struct ReservedAreas {
pub left: f32,
pub right: f32,
pub top: f32,
pub bottom: f32,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ToastSeverity {
Info,
Success,
Warning,
Error,
}
#[derive(Clone, Debug)]
pub struct ToastEntry {
pub entity: freecs::Entity,
pub spawn_time: f64,
pub duration: f32,
pub dismissing: bool,
}
pub struct DrawRectParams {
pub position: Vec2,
pub size: Vec2,
pub color: Vec4,
pub corner_radius: f32,
pub border_width: f32,
pub border_color: Vec4,
pub layer: UiLayer,
pub z_index: i32,
}
pub struct OverlayText {
pub text: String,
pub position: Vec2,
pub properties: TextProperties,
pub clip_rect: Option<Rect>,
pub layer: UiLayer,
pub z_index: i32,
}
impl RetainedUiState {
pub fn draw_overlay_rect(&mut self, rect: UiRect) {
self.overlay_rects.push(rect);
}
pub fn draw_overlay_text(
&mut self,
text: &str,
position: Vec2,
properties: TextProperties,
clip_rect: Option<Rect>,
layer: UiLayer,
z_index: i32,
) {
self.overlay_text.push(OverlayText {
text: text.to_string(),
position,
properties,
clip_rect,
layer,
z_index,
});
}
pub fn animate(
&mut self,
entity: freecs::Entity,
property: u32,
target: f32,
speed: f32,
) -> f32 {
let key = (entity, property);
let current_time = self.current_time;
let delta_time = self.delta_time;
let entry = self
.animation_states
.entry(key)
.or_insert((target, current_time));
entry.1 = current_time;
let current = entry.0;
let diff = target - current;
if diff.abs() < 0.001 {
entry.0 = target;
return target;
}
let new_value = current + diff * (speed * delta_time).min(1.0);
entry.0 = new_value;
new_value
}
pub(crate) fn register_named_property(&mut self, name: &str, property_id: PropertyId) {
debug_assert!(
!self.named_properties.contains_key(name),
"Duplicate named property: {name}"
);
self.named_properties.insert(name.to_string(), property_id);
}
pub fn register_custom_state(&mut self) -> usize {
let index = crate::ecs::ui::state::STATE_COUNT + self.custom_state_count;
self.custom_state_count += 1;
index
}
pub fn total_state_count(&self) -> usize {
crate::ecs::ui::state::STATE_COUNT + self.custom_state_count
}
pub fn remove_secondary_window(&mut self, window_index: usize) {
self.secondary_buffers.remove(&window_index);
}
pub fn cleanup_stale_animations(&mut self) {
let current_time = self.current_time;
self.animation_states
.retain(|_, (_, last_accessed)| current_time - *last_accessed < 2.0);
}
pub fn draw_rect(&mut self, position: Vec2, size: Vec2, color: Vec4) {
self.frame_rects.push(UiRect {
position,
size,
color,
corner_radius: 0.0,
border_width: 0.0,
border_color: Vec4::new(0.0, 0.0, 0.0, 0.0),
rotation: 0.0,
clip_rect: None,
layer: UiLayer::FloatingPanels,
z_index: 0,
});
}
pub fn draw_rect_styled(&mut self, params: DrawRectParams) {
self.frame_rects.push(UiRect {
position: params.position,
size: params.size,
color: params.color,
corner_radius: params.corner_radius,
border_width: params.border_width,
border_color: params.border_color,
rotation: 0.0,
clip_rect: None,
layer: params.layer,
z_index: params.z_index,
});
}
}
pub fn blend_color(from: Vec4, to: Vec4, t: f32) -> Vec4 {
if t <= 0.0 {
return from;
}
if t >= 1.0 {
return to;
}
let from_hsla = rgba_to_hsla(from);
let to_hsla = rgba_to_hsla(to);
let mut h_diff = to_hsla.x - from_hsla.x;
if h_diff > 0.5 {
h_diff -= 1.0;
} else if h_diff < -0.5 {
h_diff += 1.0;
}
let h = (from_hsla.x + h_diff * t).rem_euclid(1.0);
let s = from_hsla.y + (to_hsla.y - from_hsla.y) * t;
let l = from_hsla.z + (to_hsla.z - from_hsla.z) * t;
let a = from_hsla.w + (to_hsla.w - from_hsla.w) * t;
hsla_to_rgba(Vec4::new(h, s, l, a))
}
fn rgba_to_hsla(color: Vec4) -> Vec4 {
let r = color.x;
let g = color.y;
let b = color.z;
let a = color.w;
let max = r.max(g).max(b);
let min = r.min(g).min(b);
let l = (max + min) * 0.5;
if (max - min).abs() < f32::EPSILON {
return Vec4::new(0.0, 0.0, l, a);
}
let d = max - min;
let s = if l > 0.5 {
d / (2.0 - max - min)
} else {
d / (max + min)
};
let h = if (max - r).abs() < f32::EPSILON {
let mut h = (g - b) / d;
if g < b {
h += 6.0;
}
h / 6.0
} else if (max - g).abs() < f32::EPSILON {
((b - r) / d + 2.0) / 6.0
} else {
((r - g) / d + 4.0) / 6.0
};
Vec4::new(h, s, l, a)
}
fn hsla_to_rgba(hsla: Vec4) -> Vec4 {
let h = hsla.x;
let s = hsla.y;
let l = hsla.z;
let a = hsla.w;
if s.abs() < f32::EPSILON {
return Vec4::new(l, l, l, a);
}
let q = if l < 0.5 {
l * (1.0 + s)
} else {
l + s - l * s
};
let p = 2.0 * l - q;
let r = hue_to_rgb(p, q, h + 1.0 / 3.0);
let g = hue_to_rgb(p, q, h);
let b = hue_to_rgb(p, q, h - 1.0 / 3.0);
Vec4::new(r, g, b, a)
}
fn hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 {
if t < 0.0 {
t += 1.0;
}
if t > 1.0 {
t -= 1.0;
}
if t < 1.0 / 6.0 {
return p + (q - p) * 6.0 * t;
}
if t < 0.5 {
return q;
}
if t < 2.0 / 3.0 {
return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
}
p
}
#[cfg(test)]
mod tests {
use super::*;
fn dummy_entity(id: u32) -> freecs::Entity {
freecs::Entity { id, generation: 0 }
}
#[test]
fn button_clicked_bubbles() {
assert!(UiEvent::ButtonClicked(dummy_entity(0)).bubbles());
}
#[test]
fn slider_changed_bubbles() {
assert!(
UiEvent::SliderChanged {
entity: dummy_entity(0),
value: 0.0
}
.bubbles()
);
}
#[test]
fn tree_node_context_menu_does_not_bubble() {
assert!(
!UiEvent::TreeNodeContextMenu {
tree: dummy_entity(0),
node: dummy_entity(1),
position: Vec2::zeros(),
}
.bubbles()
);
}
#[test]
fn modal_closed_does_not_bubble() {
assert!(
!UiEvent::ModalClosed {
entity: dummy_entity(0),
confirmed: true,
}
.bubbles()
);
}
#[test]
fn target_entity_button() {
let entity = dummy_entity(42);
assert_eq!(UiEvent::ButtonClicked(entity).target_entity(), entity);
}
#[test]
fn target_entity_tree_node_selected() {
let tree = dummy_entity(1);
let node = dummy_entity(2);
assert_eq!(
UiEvent::TreeNodeSelected {
tree,
node,
selected: true
}
.target_entity(),
node
);
}
#[test]
fn target_entity_modal_closed() {
let entity = dummy_entity(5);
assert_eq!(
UiEvent::ModalClosed {
entity,
confirmed: false
}
.target_entity(),
entity
);
}
#[test]
fn all_bubbling_events_bubble() {
let entity = dummy_entity(0);
let bubbling_events = [
UiEvent::ButtonClicked(entity),
UiEvent::SliderChanged { entity, value: 0.0 },
UiEvent::ToggleChanged {
entity,
value: false,
},
UiEvent::CheckboxChanged {
entity,
value: false,
},
UiEvent::RadioChanged {
entity,
group_id: 0,
option_index: 0,
},
UiEvent::TabChanged {
entity,
tab_index: 0,
},
UiEvent::TextInputChanged {
entity,
text: String::new(),
},
UiEvent::TextInputSubmitted {
entity,
text: String::new(),
},
UiEvent::DropdownChanged {
entity,
selected_index: 0,
},
UiEvent::MenuItemClicked {
entity,
item_index: 0,
},
UiEvent::ColorPickerChanged {
entity,
color: Vec4::zeros(),
},
UiEvent::SelectableLabelClicked {
entity,
selected: false,
},
UiEvent::DragValueChanged { entity, value: 0.0 },
UiEvent::ContextMenuItemClicked {
entity,
item_index: 0,
},
UiEvent::TreeNodeSelected {
tree: entity,
node: entity,
selected: false,
},
UiEvent::TreeNodeToggled {
tree: entity,
node: entity,
expanded: false,
},
];
for event in &bubbling_events {
assert!(event.bubbles(), "Expected {:?} to bubble", event);
}
}
#[test]
fn all_non_bubbling_events_do_not_bubble() {
let entity = dummy_entity(0);
let non_bubbling_events = [
UiEvent::TreeNodeContextMenu {
tree: entity,
node: entity,
position: Vec2::zeros(),
},
UiEvent::ModalClosed {
entity,
confirmed: false,
},
UiEvent::CommandPaletteExecuted {
entity,
command_index: 0,
},
UiEvent::TileTabActivated {
container: entity,
pane_id: TileId(0),
},
UiEvent::TileTabClosed {
container: entity,
pane_id: TileId(0),
title: String::new(),
},
UiEvent::TileSplitterMoved {
container: entity,
split_id: TileId(0),
ratio: 0.5,
},
];
for event in &non_bubbling_events {
assert!(!event.bubbles(), "Expected {:?} not to bubble", event);
}
}
}