use crate::drag_drop::DragDropState;
use std::collections::HashSet;
use std::hash::Hash;
#[derive(Clone, Debug, PartialEq)]
pub struct BoxSelectionState {
pub start_pos: egui::Pos2,
pub active: bool,
}
impl BoxSelectionState {
pub fn new(start_pos: egui::Pos2) -> Self {
Self {
start_pos,
active: true,
}
}
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OutlinerState<Id>
where
Id: Hash + Eq + Clone + Send + Sync,
{
expanded: HashSet<Id>,
editing: Option<Id>,
#[cfg_attr(feature = "serde", serde(skip))]
editing_text: String,
#[cfg_attr(feature = "serde", serde(skip))]
drag_drop: DragDropState<Id>,
#[cfg_attr(feature = "serde", serde(skip))]
last_selected: Option<Id>,
#[cfg_attr(feature = "serde", serde(skip))]
box_selection: Option<BoxSelectionState>,
#[cfg_attr(feature = "serde", serde(skip))]
dragging_nodes: Vec<Id>,
}
impl<Id> Default for OutlinerState<Id>
where
Id: Hash + Eq + Clone + Send + Sync,
{
fn default() -> Self {
Self {
expanded: HashSet::new(),
editing: None,
editing_text: String::new(),
drag_drop: DragDropState::new(),
last_selected: None,
box_selection: None,
dragging_nodes: Vec::new(),
}
}
}
impl<Id> OutlinerState<Id>
where
Id: Hash + Eq + Clone + Send + Sync,
{
pub fn load(ctx: &egui::Context, id: egui::Id) -> Self
where
Id: 'static,
{
ctx.data_mut(|d| d.get_persisted(id).unwrap_or_default())
}
pub fn store(&self, ctx: &egui::Context, id: egui::Id)
where
Id: 'static,
{
ctx.data_mut(|d| d.insert_persisted(id, self.clone()));
}
pub fn is_expanded(&self, id: &Id) -> bool {
self.expanded.contains(id)
}
pub fn toggle_expanded(&mut self, id: &Id) {
if self.expanded.contains(id) {
self.expanded.remove(id);
} else {
self.expanded.insert(id.clone());
}
}
pub fn set_expanded(&mut self, id: &Id, expanded: bool) {
if expanded {
self.expanded.insert(id.clone());
} else {
self.expanded.remove(id);
}
}
pub fn is_editing(&self, id: &Id) -> bool {
self.editing.as_ref() == Some(id)
}
pub fn start_editing(&mut self, id: Id, initial_text: String) {
self.editing = Some(id);
self.editing_text = initial_text;
}
pub fn stop_editing(&mut self) {
self.editing = None;
self.editing_text.clear();
}
pub fn editing_text_mut(&mut self) -> &mut String {
&mut self.editing_text
}
pub fn editing_text(&self) -> &str {
&self.editing_text
}
pub fn drag_drop(&self) -> &DragDropState<Id> {
&self.drag_drop
}
pub fn drag_drop_mut(&mut self) -> &mut DragDropState<Id> {
&mut self.drag_drop
}
pub fn set_last_selected(&mut self, id: Option<Id>) {
self.last_selected = id;
}
pub fn last_selected(&self) -> Option<&Id> {
self.last_selected.as_ref()
}
pub fn start_box_selection(&mut self, start_pos: egui::Pos2) {
self.box_selection = Some(BoxSelectionState {
start_pos,
active: true,
});
}
pub fn box_selection(&self) -> Option<&BoxSelectionState> {
self.box_selection.as_ref()
}
pub fn end_box_selection(&mut self) {
self.box_selection = None;
}
pub fn set_dragging_nodes(&mut self, nodes: Vec<Id>) {
self.dragging_nodes = nodes;
}
pub fn dragging_nodes(&self) -> &[Id] {
&self.dragging_nodes
}
pub fn clear_dragging_nodes(&mut self) {
self.dragging_nodes.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::DropPosition;
#[test]
fn test_default_state() {
let state = OutlinerState::<String>::default();
assert!(!state.is_expanded(&"test".to_string()));
assert!(!state.is_editing(&"test".to_string()));
assert!(!state.drag_drop().is_dragging());
assert_eq!(state.last_selected(), None);
assert_eq!(state.box_selection(), None);
assert!(state.dragging_nodes().is_empty());
}
#[test]
fn test_expansion() {
let mut state = OutlinerState::<String>::default();
let id = "node1".to_string();
assert!(!state.is_expanded(&id));
state.set_expanded(&id, true);
assert!(state.is_expanded(&id));
state.set_expanded(&id, false);
assert!(!state.is_expanded(&id));
}
#[test]
fn test_toggle_expansion() {
let mut state = OutlinerState::<String>::default();
let id = "node1".to_string();
state.toggle_expanded(&id);
assert!(state.is_expanded(&id));
state.toggle_expanded(&id);
assert!(!state.is_expanded(&id));
}
#[test]
fn test_multiple_expansions() {
let mut state = OutlinerState::<String>::default();
let id1 = "node1".to_string();
let id2 = "node2".to_string();
let id3 = "node3".to_string();
state.set_expanded(&id1, true);
state.set_expanded(&id2, true);
state.set_expanded(&id3, true);
assert!(state.is_expanded(&id1));
assert!(state.is_expanded(&id2));
assert!(state.is_expanded(&id3));
state.set_expanded(&id2, false);
assert!(state.is_expanded(&id1));
assert!(!state.is_expanded(&id2));
assert!(state.is_expanded(&id3));
}
#[test]
fn test_editing() {
let mut state = OutlinerState::<String>::default();
let id1 = "node1".to_string();
let id2 = "node2".to_string();
assert!(!state.is_editing(&id1));
state.start_editing(id1.clone(), "Node 1".to_string());
assert!(state.is_editing(&id1));
assert!(!state.is_editing(&id2));
assert_eq!(state.editing_text(), "Node 1");
state.start_editing(id2.clone(), "Node 2".to_string());
assert!(!state.is_editing(&id1));
assert!(state.is_editing(&id2));
assert_eq!(state.editing_text(), "Node 2");
state.stop_editing();
assert!(!state.is_editing(&id1));
assert!(!state.is_editing(&id2));
assert_eq!(state.editing_text(), "");
}
#[test]
fn test_editing_same_node_twice() {
let mut state = OutlinerState::<String>::default();
let id = "node1".to_string();
state.start_editing(id.clone(), "First".to_string());
assert!(state.is_editing(&id));
assert_eq!(state.editing_text(), "First");
state.start_editing(id.clone(), "Second".to_string());
assert!(state.is_editing(&id));
assert_eq!(state.editing_text(), "Second");
}
#[test]
fn test_drag_drop_state_access() {
let mut state = OutlinerState::<u64>::default();
assert!(!state.drag_drop().is_dragging());
state.drag_drop_mut().start_drag(42);
assert!(state.drag_drop().is_dragging());
assert_eq!(state.drag_drop().dragging_id(), Some(&42));
}
#[test]
fn test_last_selected() {
let mut state = OutlinerState::<u64>::default();
assert_eq!(state.last_selected(), None);
state.set_last_selected(Some(1));
assert_eq!(state.last_selected(), Some(&1));
state.set_last_selected(Some(2));
assert_eq!(state.last_selected(), Some(&2));
state.set_last_selected(None);
assert_eq!(state.last_selected(), None);
}
#[test]
fn test_box_selection_lifecycle() {
let mut state = OutlinerState::<u64>::default();
assert_eq!(state.box_selection(), None);
let start_pos = egui::pos2(10.0, 20.0);
state.start_box_selection(start_pos);
let box_sel = state.box_selection();
assert!(box_sel.is_some());
assert_eq!(box_sel.unwrap().start_pos, start_pos);
assert!(box_sel.unwrap().active);
state.end_box_selection();
assert_eq!(state.box_selection(), None);
}
#[test]
fn test_box_selection_state_new() {
let pos = egui::pos2(5.0, 10.0);
let box_sel = BoxSelectionState::new(pos);
assert_eq!(box_sel.start_pos, pos);
assert!(box_sel.active);
}
#[test]
fn test_dragging_nodes() {
let mut state = OutlinerState::<u64>::default();
assert!(state.dragging_nodes().is_empty());
let nodes = vec![1, 2, 3];
state.set_dragging_nodes(nodes.clone());
assert_eq!(state.dragging_nodes(), &[1, 2, 3]);
state.clear_dragging_nodes();
assert!(state.dragging_nodes().is_empty());
}
#[test]
fn test_dragging_nodes_update() {
let mut state = OutlinerState::<u64>::default();
state.set_dragging_nodes(vec![1, 2]);
assert_eq!(state.dragging_nodes().len(), 2);
state.set_dragging_nodes(vec![3, 4, 5]);
assert_eq!(state.dragging_nodes().len(), 3);
assert_eq!(state.dragging_nodes(), &[3, 4, 5]);
}
#[test]
fn test_combined_state_operations() {
let mut state = OutlinerState::<u64>::default();
state.set_expanded(&1, true);
state.set_expanded(&2, true);
state.start_editing(3, "Node 3".to_string());
state.set_last_selected(Some(4));
state.drag_drop_mut().start_drag(5);
assert!(state.is_expanded(&1));
assert!(state.is_expanded(&2));
assert!(state.is_editing(&3));
assert_eq!(state.last_selected(), Some(&4));
assert!(state.drag_drop().is_dragging_node(&5));
}
#[test]
fn test_expansion_persistence() {
let mut state = OutlinerState::<u64>::default();
for i in 1..=10 {
state.set_expanded(&i, true);
}
for i in 1..=10 {
assert!(state.is_expanded(&i));
}
state.set_expanded(&3, false);
state.set_expanded(&7, false);
assert!(state.is_expanded(&1));
assert!(state.is_expanded(&2));
assert!(!state.is_expanded(&3));
assert!(state.is_expanded(&4));
assert!(!state.is_expanded(&7));
assert!(state.is_expanded(&10));
}
#[test]
fn test_drag_drop_integration() {
let mut state = OutlinerState::<u64>::default();
state.drag_drop_mut().start_drag(1);
state.set_dragging_nodes(vec![1, 2, 3]);
state.drag_drop_mut().update_hover(4, DropPosition::Inside);
assert!(state.drag_drop().is_dragging());
assert_eq!(state.dragging_nodes(), &[1, 2, 3]);
assert!(state.drag_drop().is_hover_target(&4));
let result = state.drag_drop_mut().end_drag();
assert!(result.is_some());
state.clear_dragging_nodes();
assert!(state.dragging_nodes().is_empty());
}
#[test]
fn test_state_isolation() {
let mut state1 = OutlinerState::<u64>::default();
let mut state2 = OutlinerState::<u64>::default();
state1.set_expanded(&1, true);
state2.set_expanded(&2, true);
assert!(state1.is_expanded(&1));
assert!(!state1.is_expanded(&2));
assert!(!state2.is_expanded(&1));
assert!(state2.is_expanded(&2));
}
}