use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use pixtuoid_core::state::{ActivityState, AgentSlot, SceneState};
use pixtuoid_core::AgentId;
pub const AUTO_COLLAPSE_THRESHOLD: usize = 5;
pub const DASHBOARD_VIEWPORT_ROWS: usize = 16;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RowState {
Active(Option<Arc<str>>),
Waiting(Arc<str>),
Idle,
}
#[derive(Debug, Clone)]
pub struct DashboardRow {
pub agent_id: AgentId,
pub parent_id: Option<AgentId>,
pub depth: u8,
pub label: Arc<str>,
pub source: Arc<str>,
pub floor_idx: usize,
pub state: RowState,
pub child_count: usize,
pub collapsed: bool,
}
#[derive(Debug, Default)]
pub struct DashboardFolds {
collapsed: HashSet<AgentId>,
user_toggled: HashSet<AgentId>,
}
impl DashboardFolds {
fn is_collapsed(&self, root_id: AgentId, child_count: usize) -> bool {
if self.user_toggled.contains(&root_id) {
self.collapsed.contains(&root_id)
} else {
child_count > AUTO_COLLAPSE_THRESHOLD
}
}
pub fn fold_all(&mut self, roots: impl IntoIterator<Item = AgentId>) {
for root in roots {
self.user_toggled.insert(root);
self.collapsed.insert(root);
}
}
pub fn unfold_all(&mut self, roots: impl IntoIterator<Item = AgentId>) {
for root in roots {
self.user_toggled.insert(root);
self.collapsed.remove(&root);
}
}
}
pub fn build_dashboard_rows(scene: &SceneState, folds: &DashboardFolds) -> Vec<DashboardRow> {
let mut children: HashMap<AgentId, Vec<AgentId>> = HashMap::new();
for (id, slot) in &scene.agents {
if let Some(parent) = slot.parent_id {
if scene.agents.contains_key(&parent) {
children.entry(parent).or_default().push(*id);
}
}
}
let mut roots: Vec<AgentId> = scene
.agents
.iter()
.filter(|(_, s)| s.parent_id.is_none_or(|p| !scene.agents.contains_key(&p)))
.map(|(id, _)| *id)
.collect();
roots.sort_by_key(|id| scene.agents[id].desk_index);
let mut rows = Vec::new();
for root in roots {
push_subtree(scene, &children, folds, root, 0, None, &mut rows);
}
rows
}
fn push_subtree(
scene: &SceneState,
children: &HashMap<AgentId, Vec<AgentId>>,
folds: &DashboardFolds,
node: AgentId,
depth: u8,
parent_id: Option<AgentId>,
rows: &mut Vec<DashboardRow>,
) {
let empty: Vec<AgentId> = Vec::new();
let kids = children.get(&node).unwrap_or(&empty);
let child_count = kids.len();
let collapsed = depth == 0 && folds.is_collapsed(node, child_count);
rows.push(row_for(
&scene.agents[&node],
parent_id,
depth,
child_count,
collapsed,
));
if collapsed {
return;
}
let mut kids = kids.clone();
kids.sort_by_key(|id| scene.agents[id].desk_index);
for kid in kids {
push_subtree(scene, children, folds, kid, depth + 1, Some(node), rows);
}
}
fn row_for(
slot: &AgentSlot,
parent_id: Option<AgentId>,
depth: u8,
child_count: usize,
collapsed: bool,
) -> DashboardRow {
DashboardRow {
agent_id: slot.agent_id,
parent_id,
depth,
label: slot.label.clone(),
source: slot.source.clone(),
floor_idx: slot.floor_idx,
state: row_state(&slot.state),
child_count,
collapsed,
}
}
fn row_state(state: &ActivityState) -> RowState {
match state {
ActivityState::Active { detail, .. } => RowState::Active(detail.clone()),
ActivityState::Waiting { reason } => RowState::Waiting(reason.clone()),
ActivityState::Idle => RowState::Idle,
}
}
pub fn move_selection(
rows: &[DashboardRow],
current: Option<AgentId>,
dir: i32,
) -> Option<AgentId> {
if rows.is_empty() {
return None;
}
let new_idx = match current.and_then(|c| rows.iter().position(|r| r.agent_id == c)) {
Some(i) => (i as i32 + dir).clamp(0, rows.len() as i32 - 1) as usize,
None => 0, };
Some(rows[new_idx].agent_id)
}
pub fn reanchor_selection(rows: &[DashboardRow], current: Option<AgentId>) -> Option<AgentId> {
match current {
Some(c) if rows.iter().any(|r| r.agent_id == c) => Some(c),
_ => rows.first().map(|r| r.agent_id),
}
}
pub fn resolve_floor(rows: &[DashboardRow], selected: AgentId) -> Option<usize> {
rows.iter()
.find(|r| r.agent_id == selected)
.map(|r| r.floor_idx)
}
pub fn clamp_scroll(
rows: &[DashboardRow],
selected: Option<AgentId>,
scroll: usize,
visible_height: usize,
) -> usize {
let Some(sel) = selected else {
return 0;
};
let Some(idx) = rows.iter().position(|r| r.agent_id == sel) else {
return scroll;
};
if idx < scroll {
idx
} else if visible_height > 0 && idx >= scroll + visible_height {
idx + 1 - visible_height
} else {
scroll
}
}
#[derive(Debug, Default)]
pub struct DashboardUi {
pub open: bool,
pub selected: Option<AgentId>,
pub scroll: usize,
pub folds: DashboardFolds,
}
#[cfg(test)]
mod tests;