use crate::graph::Node;
use crate::spacing::SpacingConfig;
use crate::style::StyleChars;
use super::canvas::Canvas;
use super::semantic::CellOwnerKind;
const CYCLE_Z_INDEX: u8 = 5;
pub fn route_cycle_edge(
from: &Node,
to: &Node,
canvas: &mut Canvas,
style: &StyleChars,
spacing: &SpacingConfig,
direction: crate::graph::Direction,
owner_id: Option<&str>,
) {
use crate::graph::Direction;
if !canvas.is_visible(from) || !canvas.is_visible(to) {
return;
}
match direction {
Direction::TD | Direction::TB | Direction::BT => {
let cycle_gutter = spacing.cycle_gutter;
if canvas.width <= cycle_gutter {
return;
}
let gutter_x = canvas.width - 2;
let from_y = from.center_y();
let to_y = to.center_y();
for x in (from.x + from.width)..gutter_x {
set_cycle_edge_char(canvas, x, from_y, style.back_h, style, owner_id);
}
let (top, bottom) = if from_y < to_y {
(from_y, to_y)
} else {
(to_y, from_y)
};
for y in top..=bottom {
set_cycle_edge_char(canvas, gutter_x, y, style.back_v, style, owner_id);
}
for x in (to.x + to.width)..gutter_x {
set_cycle_edge_char(canvas, x, to_y, style.back_h, style, owner_id);
}
if from_y < to_y {
set_cycle_char(canvas, gutter_x, from_y, style.corner_dr, owner_id); set_cycle_char(canvas, gutter_x, to_y, style.corner_ur, owner_id);
} else {
set_cycle_char(canvas, gutter_x, from_y, style.corner_ur, owner_id); set_cycle_char(canvas, gutter_x, to_y, style.corner_dr, owner_id);
}
set_cycle_char(canvas, to.x + to.width, to_y, style.arrow_left, owner_id);
}
Direction::LR | Direction::RL => {
if from.id == to.id {
let node_right = from.x + from.width;
let node_bottom = from.bottom_y();
let loop_offset = spacing.row_spacing.max(2);
let gutter_y = node_bottom + loop_offset;
if gutter_y >= canvas.height {
return;
}
let center_x = from.x + from.width / 2;
let loop_right = node_right + 2;
if loop_right >= canvas.width {
return;
}
set_cycle_char(
canvas,
loop_right,
from.center_y(),
style.corner_dr,
owner_id,
);
set_cycle_char(canvas, loop_right, gutter_y, style.corner_ur, owner_id);
set_cycle_char(canvas, center_x, gutter_y, style.corner_ul, owner_id);
for x in node_right..loop_right {
set_cycle_edge_char(canvas, x, from.center_y(), style.back_h, style, owner_id);
}
for y in (from.center_y() + 1)..gutter_y {
set_cycle_edge_char(canvas, loop_right, y, style.back_v, style, owner_id);
}
for x in (center_x + 1)..loop_right {
set_cycle_edge_char(canvas, x, gutter_y, style.back_h, style, owner_id);
}
for y in (node_bottom + 1)..gutter_y {
set_cycle_edge_char(canvas, center_x, y, style.back_v, style, owner_id);
}
if node_bottom < canvas.height {
set_cycle_char(canvas, center_x, node_bottom, style.arrow_up, owner_id);
}
return;
}
let nodes_bottom = from.bottom_y().max(to.bottom_y());
let gutter_y = nodes_bottom + spacing.row_spacing.max(2);
if gutter_y >= canvas.height {
return;
}
let from_x = from.x + from.width / 2;
let to_x = to.x + to.width / 2;
for y in from.bottom_y()..=gutter_y {
set_cycle_edge_char(canvas, from_x, y, style.back_v, style, owner_id);
}
let (left, right) = if from_x < to_x {
(from_x, to_x)
} else {
(to_x, from_x)
};
for x in left..=right {
set_cycle_edge_char(canvas, x, gutter_y, style.back_h, style, owner_id);
}
for y in to.bottom_y()..=gutter_y {
set_cycle_edge_char(canvas, to_x, y, style.back_v, style, owner_id);
}
let target_bottom_y = to.bottom_y();
if target_bottom_y < canvas.height {
set_cycle_char(canvas, to_x, target_bottom_y, style.arrow_up, owner_id);
}
}
}
}
fn set_cycle_char(canvas: &mut Canvas, x: usize, y: usize, ch: char, owner_id: Option<&str>) {
if let Some(owner_id) = owner_id {
canvas.set_owned(x, y, ch, CellOwnerKind::CycleEdge, owner_id, CYCLE_Z_INDEX);
} else {
canvas.set(x, y, ch);
}
}
fn set_cycle_edge_char(
canvas: &mut Canvas,
x: usize,
y: usize,
ch: char,
style: &StyleChars,
owner_id: Option<&str>,
) {
if let Some(owner_id) = owner_id {
canvas.set_edge_char_owned(
x,
y,
ch,
style,
CellOwnerKind::CycleEdge,
owner_id,
CYCLE_Z_INDEX,
);
} else {
canvas.set_edge_char(x, y, ch, style);
}
}
#[inline]
pub fn center_x(node: &Node) -> usize {
node.center_x()
}
#[inline]
pub fn center_y(node: &Node) -> usize {
node.center_y()
}
#[cfg(test)]
mod tests {
use super::super::canvas::Canvas;
use super::*;
use crate::graph::Direction;
use crate::spacing::SpacingConfig;
use crate::style::{BaseStyle, CompositeStyle};
fn make_node(id: &str, x: usize, y: usize, width: usize) -> Node {
Node {
id: id.into(),
label: id.into(),
label_lines: Vec::new(),
shape: crate::graph::NodeShape::Rectangle,
click_target: None,
x,
y,
width,
height: crate::style::BOX_HEIGHT,
rank: 0,
}
}
fn unicode_chars() -> StyleChars {
CompositeStyle::default().to_style_chars(BaseStyle::Unicode)
}
#[test]
fn cycle_edge_routes_through_gutter_td() {
let chars = unicode_chars();
let mut canvas = Canvas::new(80, 40);
let src = make_node("S", 10, 15, 7);
let target = make_node("T", 10, 2, 7);
let spacing = SpacingConfig::default_config();
route_cycle_edge(
&src,
&target,
&mut canvas,
&chars,
&spacing,
Direction::TD,
None,
);
let gutter_x = canvas.width - 2;
let src_mid_y = src.center_y();
let target_mid_y = target.center_y();
assert_eq!(canvas.get(gutter_x, src_mid_y), chars.corner_ur);
assert_eq!(canvas.get(gutter_x, target_mid_y), chars.corner_dr);
let mid_y = (src_mid_y + target_mid_y) / 2;
assert_eq!(canvas.get(gutter_x, mid_y), chars.back_v);
assert_eq!(
canvas.get(target.x + target.width, target_mid_y),
chars.arrow_left
);
}
#[test]
fn cycle_edge_invisible_nodes_is_noop() {
let chars = unicode_chars();
let mut canvas = Canvas::new(20, 20);
let src = make_node("S", 100, 100, 7);
let target = make_node("T", 100, 50, 7);
let spacing = SpacingConfig::default_config();
route_cycle_edge(
&src,
&target,
&mut canvas,
&chars,
&spacing,
Direction::TD,
None,
);
assert_eq!(canvas.get(18, 10), ' ');
}
#[test]
fn cycle_edge_lr_target_entry_uses_visible_arrow() {
let chars = unicode_chars();
let mut canvas = Canvas::new(80, 30);
let target = make_node("A", 8, 2, 9);
let source = make_node("B", 34, 2, 9);
let spacing = SpacingConfig::default_config();
route_cycle_edge(
&source,
&target,
&mut canvas,
&chars,
&spacing,
Direction::LR,
None,
);
let target_entry_x = target.x + target.width / 2;
let target_bottom_y = target.bottom_y();
assert_eq!(canvas.get(target_entry_x, target_bottom_y), chars.arrow_up);
assert_eq!(
canvas.get(target_entry_x, target_bottom_y + 1),
chars.back_v
);
}
#[test]
fn cycle_self_loop_lr_places_visible_arrow_below_box() {
let chars = unicode_chars();
let mut canvas = Canvas::new(40, 20);
let node = make_node("Self", 8, 2, 8);
let spacing = SpacingConfig::default_config();
route_cycle_edge(
&node,
&node,
&mut canvas,
&chars,
&spacing,
Direction::LR,
None,
);
let center_x = node.x + node.width / 2;
let loop_right = node.x + node.width + 2;
let gutter_y = node.bottom_y() + spacing.row_spacing.max(2);
assert_eq!(
canvas.get(node.x + node.width, node.center_y()),
chars.back_h
);
assert_eq!(canvas.get(loop_right, node.center_y()), chars.corner_dr);
assert_eq!(canvas.get(loop_right, gutter_y), chars.corner_ur);
assert_eq!(canvas.get(center_x, gutter_y), chars.corner_ul);
assert_eq!(canvas.get(center_x, node.bottom_y()), chars.arrow_up);
assert_eq!(canvas.get(center_x, node.bottom_y() + 1), chars.back_v);
}
#[test]
fn center_x_calculates_correctly() {
let node_odd = make_node("A", 10, 0, 7);
assert_eq!(center_x(&node_odd), 13);
let node_even = make_node("B", 10, 0, 8);
assert_eq!(center_x(&node_even), 14);
let node_min = make_node("C", 10, 0, 1);
assert_eq!(center_x(&node_min), 10);
}
}