| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | | pub use oxy_core::lanes::{width, Row}; |
| 5 | |
|
| 6 | |
|
| 7 | 16× | pub fn layout(changes: &[(String, Vec<String>)]) -> Vec<Row> { |
| 8 | 92× | let parse = |h: &str| oxy_core::hash::ObjectRef::from_hex(h).unwrap_or(oxy_core::hash::ObjectRef([0; 32])); |
| 9 | 16× | let input: Vec<(oxy_core::hash::ChangeHash, Vec<oxy_core::hash::ChangeHash>)> = |
| 10 | 55× | changes.iter().map(|(h, d)| (parse(h), d.iter().map(|x| parse(x)).collect())).collect(); |
| 11 | 16× | oxy_core::lanes::layout(&input) |
| 12 | 16× | } |
| 13 | |
|
| 14 | |
|
| 15 | |
|
| 16 | | #[derive(Clone, Debug, PartialEq)] |
| 17 | | pub struct Card { |
| 18 | | pub hash: String, |
| 19 | | pub short: String, |
| 20 | | pub title: String, |
| 21 | | pub x: usize, |
| 22 | | pub y: usize, |
| 23 | | pub head: bool, |
| 24 | | } |
| 25 | |
|
| 26 | |
|
| 27 | | #[derive(Clone, Debug, PartialEq)] |
| 28 | | pub struct Board { |
| 29 | | pub cards: Vec<Card>, |
| 30 | | |
| 31 | | pub edges: Vec<String>, |
| 32 | | pub width: usize, |
| 33 | | pub height: usize, |
| 34 | | pub lanes: usize, |
| 35 | | } |
| 36 | |
|
| 37 | | pub const CARD_W: usize = 150; |
| 38 | | pub const CARD_H: usize = 34; |
| 39 | | const GAP_X: usize = 40; |
| 40 | | const GAP_Y: usize = 22; |
| 41 | |
|
| 42 | |
|
| 43 | |
|
| 44 | |
|
| 45 | 5× | pub fn board(changes: &[crate::repos::view::ChangeView]) -> Board { |
| 46 | 15× | let input: Vec<(String, Vec<String>)> = changes.iter().map(|c| (c.hash.clone(), c.deps.clone())).collect(); |
| 47 | 5× | let rows = layout(&input); |
| 48 | 5× | let n = changes.len(); |
| 49 | 5× | let pos: std::collections::HashMap<&str, (usize, usize)> = changes |
| 50 | 5× | .iter() |
| 51 | 5× | .zip(&rows) |
| 52 | 5× | .enumerate() |
| 53 | 15× | .map(|(i, (c, r))| (c.hash.as_str(), ((n - 1 - i) * (CARD_W + GAP_X) + 10, r.lane * (CARD_H + GAP_Y) + 10))) |
| 54 | 5× | .collect(); |
| 55 | 5× | let cards = changes |
| 56 | 5× | .iter() |
| 57 | 15× | .map(|c| { |
| 58 | 15× | let (x, y) = pos[c.hash.as_str()]; |
| 59 | 15× | let mut title: String = c.title.chars().take(22).collect(); |
| 60 | 15× | if c.title.chars().count() > 22 { |
| 61 | 1× | title.push('…'); |
| 62 | 14× | } |
| 63 | 15× | Card { hash: c.hash.clone(), short: c.short.clone(), title, x, y, head: c.head } |
| 64 | 15× | }) |
| 65 | 5× | .collect(); |
| 66 | 5× | let mut edges = Vec::new(); |
| 67 | 15× | for c in changes { |
| 68 | 15× | let (cx, cy) = pos[c.hash.as_str()]; |
| 69 | 15× | for d in &c.deps { |
| 70 | 8× | if let Some(&(dx, dy)) = pos.get(d.as_str()) { |
| 71 | 8× | |
| 72 | 8× | let (x1, y1, x2, y2) = (dx + CARD_W, dy + CARD_H / 2, cx, cy + CARD_H / 2); |
| 73 | 8× | let mid = (x1 + x2) / 2; |
| 74 | 8× | edges.push(format!("M{x1} {y1} C{mid} {y1} {mid} {y2} {x2} {y2}")); |
| 75 | 8× | } |
| 76 | | } |
| 77 | | } |
| 78 | 5× | let lanes = width(&rows).max(1); |
| 79 | 5× | Board { cards, edges, width: n.max(1) * (CARD_W + GAP_X) + 20, height: lanes * (CARD_H + GAP_Y) + 20, lanes } |
| 80 | 5× | } |
| 81 | |
|
| 82 | | #[cfg(test)] |
| 83 | | mod board_tests { |
| 84 | | use super::*; |
| 85 | | use crate::repos::view::{AuthorView, ChangeView, SigView}; |
| 86 | |
|
| 87 | 4× | fn change(hash: char, title: &str, deps: &[char]) -> ChangeView { |
| 88 | 9× | let h = |c: char| std::iter::repeat_n(c, 64).collect::<String>(); |
| 89 | | ChangeView { |
| 90 | 4× | hash: h(hash), |
| 91 | 4× | short: h(hash)[..8].to_string(), |
| 92 | 4× | title: title.into(), |
| 93 | 4× | body: String::new(), |
| 94 | 4× | author: AuthorView { name: String::new(), email: String::new(), key: String::new(), unknown: true, account: None }, |
| 95 | | when_ms: 0, |
| 96 | 4× | deps: deps.iter().map(|d| h(*d)).collect(), |
| 97 | | head: false, |
| 98 | | resolution: false, |
| 99 | 4× | signature: SigView::Unsigned, |
| 100 | 4× | via: None, |
| 101 | 4× | labels: vec![], |
| 102 | | } |
| 103 | 4× | } |
| 104 | |
|
| 105 | | #[test] |
| 106 | 1× | fn independent_work_gets_its_own_lane() { |
| 107 | | |
| 108 | 1× | let changes = vec![change('c', "c on a", &['a']), change('b', "b alone", &[]), change('a', "base", &[])]; |
| 109 | 1× | let b = board(&changes); |
| 110 | 1× | assert_eq!(b.cards.len(), 3); |
| 111 | 17× | let card = |h: char| b.cards.iter().find(|c| c.hash.starts_with(h)).unwrap().clone(); |
| 112 | 1× | assert!(card('a').x < card('b').x && card('b').x < card('c').x, "oldest on the left"); |
| 113 | 1× | assert_eq!(card('a').y, card('c').y, "a chain stays in one lane"); |
| 114 | 1× | assert_ne!(card('b').y, card('a').y, "independent work in its own lane"); |
| 115 | 1× | assert_eq!(b.edges.len(), 1, "one dependency, no merge nodes"); |
| 116 | 1× | assert!(b.lanes >= 2); |
| 117 | 1× | let long = board(&[change('d', "a title far longer than the card can hold", &[])]); |
| 118 | 1× | assert!(long.cards[0].title.ends_with('…') && long.cards[0].title.chars().count() == 23); |
| 119 | 1× | } |
| 120 | | } |