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