use super::{Component, EventContext, RenderContext};
use crate::input::{Event, Key};
pub mod layout;
mod render;
mod types;
pub use layout::{LayoutEdge, LayoutNode};
pub use types::{GraphEdge, GraphNode, GraphOrientation, NodeStatus};
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub enum DependencyGraphMessage {
SetNodes(Vec<GraphNode>),
SetEdges(Vec<GraphEdge>),
AddNode(GraphNode),
AddEdge(GraphEdge),
UpdateNodeStatus {
id: String,
status: NodeStatus,
},
Clear,
SelectNext,
SelectPrev,
SelectConnected,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub enum DependencyGraphOutput {
NodeSelected(String),
StatusChanged {
id: String,
old: NodeStatus,
new_status: NodeStatus,
},
}
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct DependencyGraphState {
pub(crate) nodes: Vec<GraphNode>,
pub(crate) edges: Vec<GraphEdge>,
pub(crate) selected: Option<usize>,
pub(crate) title: Option<String>,
pub(crate) orientation: GraphOrientation,
pub(crate) show_edge_labels: bool,
}
impl DependencyGraphState {
pub fn new() -> Self {
Self::default()
}
pub fn with_node(mut self, node: GraphNode) -> Self {
self.nodes.push(node);
self
}
pub fn with_edge(mut self, edge: GraphEdge) -> Self {
self.edges.push(edge);
self
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_orientation(mut self, orientation: GraphOrientation) -> Self {
self.orientation = orientation;
self
}
pub fn with_show_edge_labels(mut self, show: bool) -> Self {
self.show_edge_labels = show;
self
}
pub fn nodes(&self) -> &[GraphNode] {
&self.nodes
}
pub fn nodes_mut(&mut self) -> &mut Vec<GraphNode> {
&mut self.nodes
}
pub fn edges(&self) -> &[GraphEdge] {
&self.edges
}
pub fn edges_mut(&mut self) -> &mut Vec<GraphEdge> {
&mut self.edges
}
pub fn selected(&self) -> Option<usize> {
self.selected
}
pub fn selected_node(&self) -> Option<&GraphNode> {
self.selected.and_then(|idx| self.nodes.get(idx))
}
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
pub fn set_title(&mut self, title: impl Into<String>) {
self.title = Some(title.into());
}
pub fn orientation(&self) -> &GraphOrientation {
&self.orientation
}
pub fn show_edge_labels(&self) -> bool {
self.show_edge_labels
}
pub fn set_orientation(&mut self, orientation: GraphOrientation) {
self.orientation = orientation;
}
pub fn set_show_edge_labels(&mut self, show: bool) {
self.show_edge_labels = show;
}
pub fn add_node(&mut self, node: GraphNode) {
self.nodes.push(node);
}
pub fn add_edge(&mut self, edge: GraphEdge) {
self.edges.push(edge);
}
pub fn clear(&mut self) {
self.nodes.clear();
self.edges.clear();
self.selected = None;
}
pub fn select_next(&mut self) -> bool {
if self.nodes.is_empty() {
return false;
}
match self.selected {
None => {
self.selected = Some(0);
true
}
Some(idx) => {
if idx + 1 < self.nodes.len() {
self.selected = Some(idx + 1);
true
} else {
self.selected = Some(0);
true
}
}
}
}
pub fn select_prev(&mut self) -> bool {
if self.nodes.is_empty() {
return false;
}
match self.selected {
None => {
self.selected = Some(self.nodes.len() - 1);
true
}
Some(idx) => {
if idx > 0 {
self.selected = Some(idx - 1);
true
} else {
self.selected = Some(self.nodes.len() - 1);
true
}
}
}
}
pub fn select_connected(&mut self) -> bool {
let selected_idx = match self.selected {
Some(idx) => idx,
None => return false,
};
let selected_id = match self.nodes.get(selected_idx) {
Some(node) => &node.id,
None => return false,
};
let target_id = self
.edges
.iter()
.find(|e| e.from == *selected_id)
.map(|e| e.to.clone());
if let Some(target) = target_id {
if let Some(target_idx) = self.nodes.iter().position(|n| n.id == target) {
self.selected = Some(target_idx);
return true;
}
}
false
}
pub fn update_node_status(&mut self, id: &str, status: NodeStatus) -> Option<NodeStatus> {
self.nodes
.iter_mut()
.find(|n| n.id == id)
.map(|node| std::mem::replace(&mut node.status, status))
}
pub fn update(&mut self, msg: DependencyGraphMessage) -> Option<DependencyGraphOutput> {
DependencyGraph::update(self, msg)
}
}
pub struct DependencyGraph;
impl Component for DependencyGraph {
type State = DependencyGraphState;
type Message = DependencyGraphMessage;
type Output = DependencyGraphOutput;
fn init() -> Self::State {
DependencyGraphState::default()
}
fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
match msg {
DependencyGraphMessage::SetNodes(nodes) => {
state.nodes = nodes;
state.selected = None;
None
}
DependencyGraphMessage::SetEdges(edges) => {
state.edges = edges;
None
}
DependencyGraphMessage::AddNode(node) => {
state.add_node(node);
None
}
DependencyGraphMessage::AddEdge(edge) => {
state.add_edge(edge);
None
}
DependencyGraphMessage::UpdateNodeStatus { id, status } => {
if let Some(old) = state.update_node_status(&id, status.clone()) {
if old != status {
Some(DependencyGraphOutput::StatusChanged {
id,
old,
new_status: status,
})
} else {
None
}
} else {
None
}
}
DependencyGraphMessage::Clear => {
state.clear();
None
}
DependencyGraphMessage::SelectNext => {
if state.select_next() {
make_selected_output(state)
} else {
None
}
}
DependencyGraphMessage::SelectPrev => {
if state.select_prev() {
make_selected_output(state)
} else {
None
}
}
DependencyGraphMessage::SelectConnected => {
if state.select_connected() {
make_selected_output(state)
} else {
None
}
}
}
}
fn handle_event(
_state: &Self::State,
event: &Event,
ctx: &EventContext,
) -> Option<Self::Message> {
if !ctx.focused || ctx.disabled {
return None;
}
if let Some(key) = event.as_key() {
match key.code {
Key::Tab if key.modifiers.shift() => Some(DependencyGraphMessage::SelectPrev),
Key::Down | Key::Char('j') | Key::Tab => Some(DependencyGraphMessage::SelectNext),
Key::Up | Key::Char('k') => Some(DependencyGraphMessage::SelectPrev),
Key::Enter | Key::Char('l') | Key::Right => {
Some(DependencyGraphMessage::SelectConnected)
}
_ => None,
}
} else {
None
}
}
fn view(state: &Self::State, ctx: &mut RenderContext<'_, '_>) {
render::render_dependency_graph(
state,
ctx.frame,
ctx.area,
ctx.theme,
ctx.focused,
ctx.disabled,
);
}
}
fn make_selected_output(state: &DependencyGraphState) -> Option<DependencyGraphOutput> {
state
.selected_node()
.map(|node| DependencyGraphOutput::NodeSelected(node.id.clone()))
}
#[cfg(test)]
mod snapshot_tests;
#[cfg(test)]
mod tests;