LCOV - code coverage report
Current view: top level - dashql-dataflow/src - layout.rs Coverage Total Hit
Test: DashQL 6e74948 Lines: 97.9 % 520 509
Test Date: 2026-11-06 17:46:50

            Line data    Source code
       1              : //! The layout of the dataflow canvas: script boxes of statement rows, flowing from left to right.
       2              : //!
       3              : //! The layout has two levels. Inside a box the statements are stacked in script order, every statement is a row
       4              : //! of the same height and the chart of a visualized statement follows its row, so the box geometry is fixed by the
       5              : //! script. Between the boxes, the layered layout of the Eclipse Layout Kernel (elk-rs) places the boxes and routes
       6              : //! the edges: every box is a node with a port per connected statement row, inputs on its left border and outputs on
       7              : //! its right border at the centers of the rows, and the edges are routed orthogonally, the edges out of one port
       8              : //! share their first segments.
       9              : //!
      10              : //! - Without hints ELK arranges the connected boxes in layers in the order of the session, the consumers of a box
      11              : //!   stack in one column. The connected parts are packed by ELK, boxes without edges go into rows below them.
      12              : //! - With hints, where the boxes were, ELK lays out interactively, every connected part on its own: the columns follow
      13              : //!   the x of the boxes, the order in a column their y, and a box keeps its y where it does not overlap another. A
      14              : //!   dropped box goes into the column under its center, or into a new column in a gap between two columns. A layout
      15              : //!   that starts from where an earlier one put every box reproduces it.
      16              : 
      17              : use std::collections::{BTreeMap, BTreeSet};
      18              : 
      19              : /// The sizes of the dataflow canvas, in canvas pixels. The defaults are the sizes of the canvas of the app and of
      20              : /// the dataflow snapshots: the app reads them from the dataflow wasm module, this is the one place to change them.
      21              : #[derive(Clone, Copy, Debug, PartialEq)]
      22              : pub struct DataflowLayoutConfig {
      23              :     /// The width of a box
      24              :     pub box_width: f64,
      25              :     /// The width of a box with a chart, a chart or a table is wider than high
      26              :     pub chart_box_width: f64,
      27              :     /// The width of the box of an input or an attachment
      28              :     pub input_box_width: f64,
      29              :     /// The height of the header of a box, a collapsed box is its header
      30              :     pub header_height: f64,
      31              :     /// The height of a statement row
      32              :     pub row_height: f64,
      33              :     /// The space between the header, the rows and the bottom of a box
      34              :     pub row_padding: f64,
      35              :     /// The height of the chart below a visualized statement
      36              :     pub chart_height: f64,
      37              :     /// The bounds of the width of a resized box
      38              :     pub min_box_width: f64,
      39              :     pub max_box_width: f64,
      40              :     /// The bounds of the height of a resized chart
      41              :     pub min_chart_height: f64,
      42              :     pub max_chart_height: f64,
      43              :     /// The least horizontal space between the columns, wider where the edges between them need more rails
      44              :     pub layer_gap: f64,
      45              :     /// The least vertical space between the boxes of a column
      46              :     pub box_gap: f64,
      47              :     /// The space between two parallel edges
      48              :     pub edge_gap: f64,
      49              :     /// The space between an edge and a box
      50              :     pub edge_box_gap: f64,
      51              :     /// A dropped box that is this close to lining up a row with a row it is connected to lines it up, so that the
      52              :     /// edge between them runs straight instead of making a small hop
      53              :     pub align_snap: f64,
      54              :     /// The boxes without edges per row
      55              :     pub isolated_per_row: usize,
      56              :     /// The grid that drags and resizes on the canvas snap to, the layout does not snap
      57              :     pub grid: f64,
      58              : }
      59              : 
      60              : impl Default for DataflowLayoutConfig {
      61           55 :     fn default() -> Self {
      62           55 :         Self {
      63           55 :             box_width: 280.0,
      64           55 :             chart_box_width: 480.0,
      65           55 :             input_box_width: 200.0,
      66           55 :             header_height: 32.0,
      67           55 :             row_height: 32.0,
      68           55 :             row_padding: 8.0,
      69           55 :             chart_height: 200.0,
      70           55 :             min_box_width: 200.0,
      71           55 :             max_box_width: 1600.0,
      72           55 :             min_chart_height: 96.0,
      73           55 :             max_chart_height: 1200.0,
      74           55 :             layer_gap: 144.0,
      75           55 :             box_gap: 48.0,
      76           55 :             edge_gap: 16.0,
      77           55 :             edge_box_gap: 24.0,
      78           55 :             align_snap: 24.0,
      79           55 :             isolated_per_row: 4,
      80           55 :             grid: 8.0,
      81           55 :         }
      82           55 :     }
      83              : }
      84              : 
      85              : /// A script box
      86              : #[derive(Clone, Debug, Default, PartialEq)]
      87              : pub struct DataflowLayoutBox {
      88              :     /// The number of statements
      89              :     pub statements: u32,
      90              :     /// The statement whose chart follows its row
      91              :     pub chart_statement: Option<u32>,
      92              :     /// Is the box collapsed to its header?
      93              :     pub collapsed: bool,
      94              :     /// The width of the box, the width of the config if absent, the chart box width with a chart
      95              :     pub width: Option<f64>,
      96              :     /// The height of the chart, the chart height of the config if absent
      97              :     pub chart_height: Option<f64>,
      98              :     /// Is the box an input or an attachment? It is `input_box_width` wide by default.
      99              :     pub input: bool,
     100              :     /// Where the box was, e.g. saved in its `@flow` tag or where the last layout put it
     101              :     pub hint: Option<(f64, f64)>,
     102              :     /// Was the box just dropped at its hint by the user? The hints of the other boxes place the layout.
     103              :     pub dropped: bool,
     104              : }
     105              : 
     106              : /// An edge between statements of two boxes
     107              : #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     108              : pub struct DataflowLayoutEdge {
     109              :     pub from_box: u32,
     110              :     pub from_statement: u32,
     111              :     pub to_box: u32,
     112              :     pub to_statement: u32,
     113              : }
     114              : 
     115              : /// The geometry of a box
     116              : #[derive(Clone, Debug, Default, PartialEq)]
     117              : pub struct DataflowBoxGeometry {
     118              :     pub x: f64,
     119              :     pub y: f64,
     120              :     pub width: f64,
     121              :     pub height: f64,
     122              :     /// The top of every statement row, relative to the box. The rows of a collapsed box are its header.
     123              :     pub rows: Vec<f64>,
     124              : }
     125              : 
     126              : impl DataflowBoxGeometry {
     127              :     /// The vertical center of the row of a statement, relative to the box
     128          356 :     pub(crate) fn port(&self, statement: u32, config: &DataflowLayoutConfig) -> f64 {
     129          356 :         match self.rows.get(statement as usize) {
     130          356 :             Some(&top) if self.height > config.header_height => top + config.row_height / 2.0,
     131            2 :             _ => config.header_height / 2.0,
     132              :         }
     133          356 :     }
     134              : }
     135              : 
     136              : /// The route of an edge between statements of two boxes, by the box and statement of its source and its target: the
     137              : /// points of a polyline of horizontal and vertical segments from the right border of the source box to the left
     138              : /// border of the target box
     139              : pub type DataflowEdgeRoute = ((u32, u32, u32, u32), Vec<(f64, f64)>);
     140              : 
     141              : /// The geometry of a box without its position
     142          237 : fn box_geometry(b: &DataflowLayoutBox, config: &DataflowLayoutConfig) -> DataflowBoxGeometry {
     143          237 :     let default_width = if b.input {
     144            9 :         config.input_box_width
     145          228 :     } else if b.chart_statement.is_some() {
     146           31 :         config.chart_box_width
     147              :     } else {
     148          197 :         config.box_width
     149              :     };
     150          237 :     let mut geometry = DataflowBoxGeometry { width: b.width.unwrap_or(default_width), ..Default::default() };
     151          237 :     if b.collapsed || b.statements == 0 {
     152            5 :         geometry.rows = vec![0.0; b.statements as usize];
     153            5 :         geometry.height = config.header_height;
     154            5 :         return geometry;
     155          232 :     }
     156          232 :     let mut y = config.header_height + config.row_padding;
     157          269 :     for statement in 0..b.statements {
     158          269 :         geometry.rows.push(y);
     159          269 :         y += config.row_height;
     160          269 :         if b.chart_statement == Some(statement) {
     161           31 :             y += config.row_padding + b.chart_height.unwrap_or(config.chart_height);
     162          238 :         }
     163              :     }
     164          232 :     geometry.height = y + config.row_padding;
     165          232 :     geometry
     166          237 : }
     167              : 
     168              : /// The edges between different boxes
     169           62 : fn box_edges(edges: &[DataflowLayoutEdge], count: usize) -> Vec<DataflowLayoutEdge> {
     170          142 :     edges.iter().filter(|e| e.from_box != e.to_box && (e.from_box as usize) < count && (e.to_box as usize) < count).copied().collect()
     171           62 : }
     172              : 
     173              : /// The connected parts of the graph, by their first box. Boxes without edges are left out.
     174           23 : fn components(count: usize, edges: &[DataflowLayoutEdge]) -> Vec<Vec<usize>> {
     175           23 :     let mut parent: Vec<usize> = (0..count).collect();
     176          246 :     fn find(parent: &mut [usize], i: usize) -> usize {
     177          246 :         let mut root = i;
     178          332 :         while parent[root] != root {
     179           86 :             root = parent[root];
     180           86 :         }
     181          246 :         parent[i] = root;
     182          246 :         root
     183          246 :     }
     184           70 :     for e in edges {
     185           70 :         let (a, b) = (find(&mut parent, e.from_box as usize), find(&mut parent, e.to_box as usize));
     186           70 :         parent[a.max(b)] = a.min(b);
     187           70 :     }
     188           70 :     let connected: BTreeSet<usize> = edges.iter().flat_map(|e| [e.from_box as usize, e.to_box as usize]).collect();
     189           23 :     let mut out: Vec<Vec<usize>> = Vec::new();
     190           23 :     let mut roots: Vec<usize> = Vec::new();
     191          111 :     for i in (0..count).filter(|i| connected.contains(i)) {
     192          106 :         let root = find(&mut parent, i);
     193          106 :         match roots.iter().position(|&r| r == root) {
     194           70 :             Some(c) => out[c].push(i),
     195           36 :             None => {
     196           36 :                 roots.push(root);
     197           36 :                 out.push(vec![i]);
     198           36 :             }
     199              :         }
     200              :     }
     201           23 :     out
     202           23 : }
     203              : 
     204              : /// Put boxes into rows of `isolated_per_row` from a top left corner, returns the bottom of the rows with a gap
     205           69 : fn place_in_rows(boxes: &[usize], (left, mut top): (f64, f64), config: &DataflowLayoutConfig, geometry: &mut [DataflowBoxGeometry]) -> f64 {
     206           69 :     for row in boxes.chunks(config.isolated_per_row.max(1)) {
     207           17 :         let mut x = left;
     208           17 :         let mut height: f64 = 0.0;
     209           27 :         for &i in row {
     210           27 :             geometry[i].x = x;
     211           27 :             geometry[i].y = top;
     212           27 :             x += geometry[i].width + config.box_gap;
     213           27 :             height = height.max(geometry[i].height);
     214           27 :         }
     215           17 :         top += height + config.box_gap;
     216              :     }
     217           69 :     top
     218           69 : }
     219              : 
     220              : /// Lay out the boxes
     221           17 : pub fn compute_dataflow_layout(boxes: &[DataflowLayoutBox], edges: &[DataflowLayoutEdge], config: &DataflowLayoutConfig) -> Vec<DataflowBoxGeometry> {
     222           17 :     compute_dataflow_layout_with_routes(boxes, edges, config).0
     223           17 : }
     224              : 
     225              : /// Lay out the boxes, with the routes of the edges between boxes
     226           62 : pub fn compute_dataflow_layout_with_routes(
     227           62 :     boxes: &[DataflowLayoutBox],
     228           62 :     edges: &[DataflowLayoutEdge],
     229           62 :     config: &DataflowLayoutConfig,
     230           62 : ) -> (Vec<DataflowBoxGeometry>, Vec<DataflowEdgeRoute>) {
     231          233 :     let mut geometry: Vec<DataflowBoxGeometry> = boxes.iter().map(|b| box_geometry(b, config)).collect();
     232           62 :     let edges = box_edges(edges, boxes.len());
     233              :     // A drop is laid out twice: the second layout starts where the first one put every box, like a reload does, so
     234              :     // the canvas shows what a reload shows
     235           62 :     let routes = if boxes.iter().any(|b| b.dropped) {
     236            7 :         arrange(boxes, &edges, config, &mut geometry);
     237            7 :         let settled: Vec<DataflowLayoutBox> =
     238           39 :             boxes.iter().zip(geometry.iter()).map(|(b, g)| DataflowLayoutBox { hint: Some((g.x, g.y)), dropped: false, ..b.clone() }).collect();
     239            7 :         arrange(&settled, &edges, config, &mut geometry)
     240              :     } else {
     241           55 :         arrange(boxes, &edges, config, &mut geometry)
     242              :     };
     243           62 :     (geometry, routes)
     244           62 : }
     245              : 
     246              : /// How far apart the seeds of two columns are, more than the widest box
     247              : const COLUMN_SPREAD: f64 = 10000.0;
     248              : 
     249              : /// The column of a center x: the columns are the runs of overlapping x ranges of the boxes that stay, a center in the
     250              : /// gap between two columns is a new column between them. Returns the index of the column, halves for the gaps.
     251          106 : fn column_of(columns: &[(f64, f64)], center: f64) -> f64 {
     252          182 :     for (i, &(x0, x1)) in columns.iter().enumerate() {
     253          182 :         if center < x0 {
     254            3 :             return i as f64 - 0.5;
     255          179 :         }
     256          179 :         if center <= x1 {
     257           97 :             return i as f64;
     258           82 :         }
     259              :     }
     260            6 :     columns.len() as f64 - 0.5
     261          106 : }
     262              : 
     263              : /// Arrange the boxes once, from their hints if they have any
     264           69 : fn arrange(boxes: &[DataflowLayoutBox], edges: &[DataflowLayoutEdge], config: &DataflowLayoutConfig, geometry: &mut [DataflowBoxGeometry]) -> Vec<DataflowEdgeRoute> {
     265           69 :     let count = geometry.len();
     266          168 :     let linked: BTreeSet<usize> = edges.iter().flat_map(|e| [e.from_box as usize, e.to_box as usize]).collect();
     267          186 :     if !boxes.iter().any(|b| b.hint.is_some()) {
     268              :         // ELK packs the connected boxes, the boxes without edges go into rows below them
     269          161 :         let connected: Vec<usize> = (0..count).filter(|b| linked.contains(b)).collect();
     270           46 :         let routes = if connected.is_empty() { Vec::new() } else { elk(boxes, edges, config, geometry, &connected, &vec![None; count], false) };
     271          136 :         let left = connected.iter().map(|&b| geometry[b].x).fold(f64::INFINITY, f64::min);
     272          136 :         let bottom = connected.iter().map(|&b| geometry[b].y + geometry[b].height).fold(f64::NEG_INFINITY, f64::max);
     273           46 :         let corner = if left.is_finite() { (left, bottom + config.box_gap * 2.0) } else { (0.0, 0.0) };
     274          161 :         let isolated: Vec<usize> = (0..count).filter(|b| !linked.contains(b)).collect();
     275           46 :         place_in_rows(&isolated, corner, config, geometry);
     276           46 :         return routes;
     277           23 :     }
     278              :     // Interactive: every connected part on its own, so that moving a box in one part leaves the others alone
     279           23 :     let mut routes = Vec::new();
     280           23 :     let mut fresh: Vec<Vec<usize>> = Vec::new();
     281           37 :     for part in components(count, edges) {
     282           38 :         if !part.iter().any(|&b| boxes[b].hint.is_some()) {
     283            0 :             fresh.push(part);
     284            0 :             continue;
     285           36 :         }
     286              :         // A box without a hint starts right of its producers or left of its consumers that have one, with the row of
     287              :         // the edge level with the row at its other end
     288           36 :         let mut seeds: Vec<Option<(f64, f64)>> = boxes.iter().map(|b| b.hint).collect();
     289          106 :         for &b in &part {
     290          106 :             if seeds[b].is_some() {
     291          100 :                 continue;
     292            6 :             }
     293            6 :             let producers: Vec<(f64, f64)> = edges
     294            6 :                 .iter()
     295           12 :                 .filter(|e| e.to_box as usize == b)
     296            6 :                 .filter_map(|e| {
     297            5 :                     let p = &geometry[e.from_box as usize];
     298            5 :                     boxes[e.from_box as usize].hint.map(|(x, y)| (x + p.width, y + p.port(e.from_statement, config) - geometry[b].port(e.to_statement, config)))
     299            5 :                 })
     300            6 :                 .collect();
     301            6 :             let consumers: Vec<(f64, f64)> = edges
     302            6 :                 .iter()
     303           12 :                 .filter(|e| e.from_box as usize == b)
     304            6 :                 .filter_map(|e| {
     305            2 :                     let c = &geometry[e.to_box as usize];
     306            2 :                     boxes[e.to_box as usize].hint.map(|(x, y)| (x, y + c.port(e.to_statement, config) - geometry[b].port(e.from_statement, config)))
     307            2 :                 })
     308            6 :                 .collect();
     309            6 :             seeds[b] = if let Some(&(x, y)) = producers.iter().max_by(|a, b| a.0.total_cmp(&b.0)) {
     310            4 :                 Some((x + config.layer_gap, y))
     311              :             } else {
     312            2 :                 consumers.iter().min_by(|a, b| a.0.total_cmp(&b.0)).map(|&(x, y)| (x - config.layer_gap - geometry[b].width, y))
     313              :             };
     314              :         }
     315              :         // A dropped box that almost lines up a row with a connected row of a box that stays lines it up
     316          106 :         for &b in part.iter().filter(|&&b| boxes[b].dropped) {
     317            7 :             let Some((x, y)) = seeds[b] else { continue };
     318           26 :             let deltas = edges.iter().filter_map(|e| {
     319           26 :                 let (this, this_statement, other, other_statement) = if e.to_box as usize == b {
     320            7 :                     (b, e.to_statement, e.from_box as usize, e.from_statement)
     321           19 :                 } else if e.from_box as usize == b {
     322            2 :                     (b, e.from_statement, e.to_box as usize, e.to_statement)
     323              :                 } else {
     324           17 :                     return None;
     325              :                 };
     326            9 :                 let (_, other_y) = boxes[other].hint.filter(|_| !boxes[other].dropped)?;
     327            9 :                 Some(other_y + geometry[other].port(other_statement, config) - (y + geometry[this].port(this_statement, config)))
     328           26 :             });
     329            9 :             if let Some(delta) = deltas.filter(|d| d.abs() <= config.align_snap).min_by(|a, b| a.abs().total_cmp(&b.abs())) {
     330            2 :                 seeds[b] = Some((x, y + delta));
     331            5 :             }
     332              :         }
     333              :         // ELK forms its columns from overlapping x ranges, a wide or dropped box would merge two columns. The seeds put
     334              :         // every box at the column of its center instead, far apart: the boxes that stay form the columns.
     335           36 :         let mut ranges: Vec<(f64, f64)> =
     336          106 :             part.iter().filter(|&&b| !boxes[b].dropped).filter_map(|&b| boxes[b].hint.map(|(x, _)| (x, x + geometry[b].width))).collect();
     337           62 :         ranges.sort_by(|a, b| a.0.total_cmp(&b.0));
     338           36 :         let mut columns: Vec<(f64, f64)> = Vec::new();
     339           93 :         for (x0, x1) in ranges {
     340           93 :             match columns.last_mut() {
     341           57 :                 Some(last) if x0 < last.1 => last.1 = last.1.max(x1),
     342           79 :                 _ => columns.push((x0, x1)),
     343              :             }
     344              :         }
     345          106 :         for &b in &part {
     346          106 :             if let Some((x, y)) = seeds[b] {
     347          106 :                 seeds[b] = Some((column_of(&columns, x + geometry[b].width / 2.0) * COLUMN_SPREAD, y));
     348          106 :             }
     349              :         }
     350           36 :         routes.extend(elk(boxes, edges, config, geometry, &part, &seeds, true));
     351              :     }
     352              :     // Boxes without edges keep their hints, the others and the parts without any hint go below everything
     353          111 :     let mut placed: Vec<usize> = (0..count).filter(|&b| linked.contains(&b) && !fresh.iter().any(|p| p.contains(&b))).collect();
     354           23 :     let mut loose: Vec<usize> = Vec::new();
     355          111 :     for b in (0..count).filter(|b| !linked.contains(b)) {
     356            5 :         match boxes[b].hint {
     357            3 :             Some((x, y)) => {
     358            3 :                 geometry[b].x = x;
     359            3 :                 geometry[b].y = y;
     360            3 :                 placed.push(b);
     361            3 :             }
     362            2 :             None => loose.push(b),
     363              :         }
     364              :     }
     365          109 :     let left = placed.iter().map(|&b| geometry[b].x).fold(f64::INFINITY, f64::min);
     366           23 :     let left = if left.is_finite() { left } else { 0.0 };
     367          109 :     let bottom = placed.iter().map(|&b| geometry[b].y + geometry[b].height).fold(f64::NEG_INFINITY, f64::max);
     368           23 :     let mut top = if bottom.is_finite() { bottom + config.box_gap * 2.0 } else { 0.0 };
     369           23 :     for part in fresh {
     370            0 :         let part_routes = elk(boxes, edges, config, geometry, &part, &vec![None; count], false);
     371            0 :         let (x0, y0) = part.iter().fold((f64::INFINITY, f64::INFINITY), |(x, y), &b| (x.min(geometry[b].x), y.min(geometry[b].y)));
     372            0 :         let (dx, dy) = (left - x0, top - y0);
     373            0 :         for &b in &part {
     374            0 :             geometry[b].x += dx;
     375            0 :             geometry[b].y += dy;
     376            0 :         }
     377            0 :         top = part.iter().map(|&b| geometry[b].y + geometry[b].height).fold(top, f64::max) + config.box_gap * 2.0;
     378            0 :         routes.extend(part_routes.into_iter().map(|(k, points)| (k, points.into_iter().map(|(x, y)| (x + dx, y + dy)).collect())));
     379              :     }
     380           23 :     place_in_rows(&loose, (left, top), config, geometry);
     381           23 :     routes
     382           69 : }
     383              : 
     384              : /// Lay out a subset of the boxes with ELK, interactively at the seeds of the boxes, and move an interactive layout
     385              : /// back by the offset that most of its boxes with hints share: ELK moves a layout to the origin.
     386           76 : fn elk(
     387           76 :     boxes: &[DataflowLayoutBox],
     388           76 :     edges: &[DataflowLayoutEdge],
     389           76 :     config: &DataflowLayoutConfig,
     390           76 :     geometry: &mut [DataflowBoxGeometry],
     391           76 :     subset: &[usize],
     392           76 :     seeds: &[Option<(f64, f64)>],
     393           76 :     interactive: bool,
     394           76 : ) -> Vec<DataflowEdgeRoute> {
     395              :     use org_eclipse_elk_alg_layered::org::eclipse::elk::alg::layered::layered_layout_provider::LayeredLayoutProvider;
     396              :     use org_eclipse_elk_alg_layered::org::eclipse::elk::alg::layered::options::{
     397              :         CrossingMinimizationStrategy, CycleBreakingStrategy, LayeredOptions, LayeringStrategy, NodePlacementStrategy,
     398              :         OrderingStrategy,
     399              :     };
     400              :     use org_eclipse_elk_alg_layered::org::eclipse::elk::alg::layered::plain_java_initialization::initialize_plain_java_layout;
     401              :     use org_eclipse_elk_core::org::eclipse::elk::core::math::ElkPadding;
     402              :     use org_eclipse_elk_core::org::eclipse::elk::core::options::{CoreOptions, Direction, EdgeRouting, PortConstraints, PortSide, SizeConstraint};
     403              :     use org_eclipse_elk_core::org::eclipse::elk::core::util::BasicProgressMonitor;
     404              :     use org_eclipse_elk_core::org::eclipse::elk::core::IGraphLayoutEngine;
     405              :     use org_eclipse_elk_graph::org::eclipse::elk::graph::properties::Property;
     406              :     use org_eclipse_elk_graph::org::eclipse::elk::graph::util::ElkGraphUtil;
     407              :     use org_eclipse_elk_graph::org::eclipse::elk::graph::{ElkConnectableShapeRef, ElkNodeRef, ElkPortRef};
     408              : 
     409         1656 :     fn set_node<T: Clone + Send + Sync + 'static>(node: &ElkNodeRef, property: &Property<T>, value: T) {
     410         1656 :         node.borrow_mut().connectable().shape().graph_element().properties_mut().set_property(property, Some(value));
     411         1656 :     }
     412          305 :     fn set_port<T: Clone + Send + Sync + 'static>(port: &ElkPortRef, property: &Property<T>, value: T) {
     413          305 :         port.borrow_mut().connectable().shape().graph_element().properties_mut().set_property(property, Some(value));
     414          305 :     }
     415              : 
     416           76 :     initialize_plain_java_layout();
     417           76 :     let graph = ElkGraphUtil::create_graph();
     418           76 :     set_node(&graph, CoreOptions::DIRECTION, Direction::Right);
     419           76 :     set_node(&graph, CoreOptions::PADDING, ElkPadding::with_any(0.0));
     420           76 :     set_node(&graph, LayeredOptions::SPACING_NODE_NODE, config.box_gap);
     421           76 :     set_node(&graph, LayeredOptions::SPACING_NODE_NODE_BETWEEN_LAYERS, config.layer_gap);
     422           76 :     set_node(&graph, LayeredOptions::SPACING_COMPONENT_COMPONENT, config.box_gap * 2.0);
     423           76 :     set_node(&graph, LayeredOptions::SPACING_EDGE_EDGE, config.edge_gap);
     424           76 :     set_node(&graph, LayeredOptions::SPACING_EDGE_EDGE_BETWEEN_LAYERS, config.edge_gap);
     425           76 :     set_node(&graph, LayeredOptions::SPACING_EDGE_NODE, config.edge_box_gap);
     426           76 :     set_node(&graph, LayeredOptions::SPACING_EDGE_NODE_BETWEEN_LAYERS, config.edge_box_gap);
     427              :     // Orthogonal routes, the edges out of one port share their first segments
     428           76 :     set_node(&graph, LayeredOptions::EDGE_ROUTING, EdgeRouting::Orthogonal);
     429           76 :     set_node(&graph, LayeredOptions::MERGE_EDGES, true);
     430           76 :     if interactive {
     431           36 :         // The phases keep what the user arranged: the cycles, the columns by x, the order in a column by y and the y
     432           36 :         // of the boxes. A part is laid out on its own.
     433           36 :         set_node(&graph, LayeredOptions::CYCLE_BREAKING_STRATEGY, CycleBreakingStrategy::Interactive);
     434           36 :         set_node(&graph, LayeredOptions::LAYERING_STRATEGY, LayeringStrategy::Interactive);
     435           36 :         set_node(&graph, LayeredOptions::CROSSING_MINIMIZATION_STRATEGY, CrossingMinimizationStrategy::Interactive);
     436           36 :         set_node(&graph, LayeredOptions::NODE_PLACEMENT_STRATEGY, NodePlacementStrategy::Interactive);
     437           36 :         set_node(&graph, LayeredOptions::CONSIDER_MODEL_ORDER_STRATEGY, OrderingStrategy::None);
     438           36 :         set_node(&graph, LayeredOptions::SEPARATE_CONNECTED_COMPONENTS, false);
     439           62 :     } else {
     440           40 :         // The order of the session, ELK packs the parts
     441           40 :         set_node(&graph, LayeredOptions::NODE_PLACEMENT_STRATEGY, NodePlacementStrategy::NetworkSimplex);
     442           40 :         set_node(&graph, LayeredOptions::CONSIDER_MODEL_ORDER_STRATEGY, OrderingStrategy::NodesAndEdges);
     443           40 :         set_node(&graph, LayeredOptions::SEPARATE_CONNECTED_COMPONENTS, true);
     444           40 :     }
     445              : 
     446           76 :     let mut in_subset = vec![false; geometry.len()];
     447          242 :     for &b in subset {
     448          242 :         in_subset[b] = true;
     449          242 :     }
     450          228 :     let edges: Vec<DataflowLayoutEdge> = edges.iter().filter(|e| in_subset[e.from_box as usize] && in_subset[e.to_box as usize]).copied().collect();
     451           76 :     let mut connected: Vec<(BTreeSet<u32>, BTreeSet<u32>)> = vec![Default::default(); geometry.len()];
     452           76 :     let mut unique: BTreeSet<(u32, u32, u32, u32)> = BTreeSet::new();
     453          168 :     for e in &edges {
     454          168 :         connected[e.to_box as usize].0.insert(e.to_statement);
     455          168 :         connected[e.from_box as usize].1.insert(e.from_statement);
     456          168 :         unique.insert((e.from_box, e.from_statement, e.to_box, e.to_statement));
     457          168 :     }
     458           76 :     let mut nodes = Vec::with_capacity(subset.len());
     459           76 :     let mut ports: BTreeMap<(u32, bool, u32), ElkPortRef> = BTreeMap::new();
     460          242 :     for &b in subset {
     461          242 :         let (g, (ins, outs)) = (&geometry[b], &connected[b]);
     462          242 :         let node = ElkGraphUtil::create_node(Some(graph.clone()));
     463          242 :         node.borrow_mut().connectable().shape().set_dimensions(g.width, g.height);
     464          242 :         if let Some((x, y)) = seeds[b] {
     465          106 :             node.borrow_mut().connectable().shape().set_location(x, y);
     466          185 :         }
     467          242 :         set_node(&node, CoreOptions::NODE_SIZE_CONSTRAINTS, SizeConstraint::fixed());
     468          242 :         set_node(&node, CoreOptions::PORT_CONSTRAINTS, PortConstraints::FixedPos);
     469          484 :         for (output, statements) in [(false, ins), (true, outs)] {
     470          484 :             for &statement in statements {
     471          305 :                 let port = ElkGraphUtil::create_port(Some(node.clone()));
     472          305 :                 port.borrow_mut().connectable().shape().set_location(if output { g.width } else { 0.0 }, g.port(statement, config));
     473          305 :                 set_port(&port, CoreOptions::PORT_SIDE, if output { PortSide::East } else { PortSide::West });
     474          305 :                 ports.insert((b as u32, output, statement), port);
     475              :             }
     476              :         }
     477          242 :         nodes.push(node);
     478              :     }
     479           76 :     let mut elk_edges = Vec::new();
     480          168 :     for (from, fs, to, ts) in unique {
     481          168 :         let (Some(source), Some(target)) = (ports.get(&(from, true, fs)), ports.get(&(to, false, ts))) else { continue };
     482          168 :         let edge = ElkGraphUtil::create_simple_edge(ElkConnectableShapeRef::Port(source.clone()), ElkConnectableShapeRef::Port(target.clone()));
     483          168 :         elk_edges.push(((from, fs, to, ts), edge));
     484              :     }
     485           76 :     LayeredLayoutProvider::new().layout(&graph, &mut BasicProgressMonitor::new());
     486          242 :     for (&b, node) in subset.iter().zip(&nodes) {
     487          242 :         let mut node = node.borrow_mut();
     488          242 :         let shape = node.connectable().shape();
     489          242 :         geometry[b].x = shape.x();
     490          242 :         geometry[b].y = shape.y();
     491          242 :     }
     492              :     // ELK moves the layout to the origin, an interactive layout moves back by the offset that most of its boxes with
     493              :     // hints share: the boxes that ELK left where they were. A dropped box does not count.
     494           76 :     let mut offset = (0.0, 0.0);
     495           76 :     if interactive {
     496          106 :         let anchors = subset.iter().filter(|&&b| boxes[b].hint.is_some() && !boxes[b].dropped).count();
     497           36 :         let mut counts: Vec<((i64, i64), usize)> = Vec::new();
     498          106 :         for &b in subset.iter().filter(|&&b| !(boxes[b].dropped && anchors > 0)) {
     499           99 :             if let Some((x, y)) = boxes[b].hint {
     500           93 :                 let key = ((x - geometry[b].x).round() as i64, (y - geometry[b].y).round() as i64);
     501           93 :                 match counts.iter_mut().find(|(k, _)| *k == key) {
     502           56 :                     Some((_, n)) => *n += 1,
     503           37 :                     None => counts.push((key, 1)),
     504              :                 }
     505            6 :             }
     506              :         }
     507              :         // The most frequent offset, the first one among equals
     508           37 :         if let Some(((dx, dy), _)) = counts.iter().fold(None::<&((i64, i64), usize)>, |best, c| if best.is_none_or(|b| c.1 > b.1) { Some(c) } else { best }) {
     509           36 :             offset = (*dx as f64, *dy as f64);
     510           36 :         }
     511          106 :         for &b in subset {
     512          106 :             geometry[b].x += offset.0;
     513          106 :             geometry[b].y += offset.1;
     514          106 :         }
     515           40 :     }
     516           76 :     let mut routes: Vec<DataflowEdgeRoute> = elk_edges
     517           76 :         .into_iter()
     518          168 :         .map(|(key, edge)| {
     519          168 :             let mut points = Vec::new();
     520          168 :             let mut edge = edge.borrow_mut();
     521          168 :             for section in edge.sections().iter() {
     522          168 :                 let mut section = section.borrow_mut();
     523          168 :                 points.push((section.start_x() + offset.0, section.start_y() + offset.1));
     524          168 :                 points.extend(section.bend_points().iter().map(|p| (p.borrow().x() + offset.0, p.borrow().y() + offset.1)));
     525          168 :                 points.push((section.end_x() + offset.0, section.end_y() + offset.1));
     526              :             }
     527          168 :             (key, points)
     528          168 :         })
     529           76 :         .collect();
     530           76 :     center_rails(&mut routes, subset, geometry);
     531           76 :     routes
     532           76 : }
     533              : 
     534              : /// ELK puts the vertical segments of the edges between two columns next to the left column, center them in the gap
     535              : /// instead: away from the boxes on both sides. The segments of a gap move together, so their order and the space
     536              : /// between them stay.
     537           76 : fn center_rails(routes: &mut [DataflowEdgeRoute], subset: &[usize], geometry: &[DataflowBoxGeometry]) {
     538          242 :     let mut ranges: Vec<(f64, f64)> = subset.iter().map(|&b| (geometry[b].x, geometry[b].x + geometry[b].width)).collect();
     539          202 :     ranges.sort_by(|a, b| a.0.total_cmp(&b.0));
     540           76 :     let mut columns: Vec<(f64, f64)> = Vec::new();
     541          242 :     for (x0, x1) in ranges {
     542          242 :         match columns.last_mut() {
     543          166 :             Some(last) if x0 < last.1 => last.1 = last.1.max(x1),
     544          184 :             _ => columns.push((x0, x1)),
     545              :         }
     546              :     }
     547          108 :     for gap in columns.windows(2).map(|w| (w[0].1, w[1].0)) {
     548          630 :         let inside = |x: f64| x > gap.0 && x < gap.1;
     549          108 :         let rails: Vec<f64> =
     550          508 :             routes.iter().flat_map(|(_, points)| points.windows(2).filter(|w| w[0].0 == w[1].0 && inside(w[0].0)).map(|w| w[0].0)).collect();
     551          108 :         let (Some(lo), Some(hi)) = (rails.iter().copied().reduce(f64::min), rails.iter().copied().reduce(f64::max)) else { continue };
     552           50 :         let shift = ((gap.0 + gap.1) / 2.0 - (lo + hi) / 2.0).round();
     553          155 :         for (_, points) in routes.iter_mut() {
     554          508 :             for p in points.iter_mut().filter(|p| inside(p.0)) {
     555          132 :                 p.0 += shift;
     556          132 :             }
     557              :         }
     558              :     }
     559           76 : }
     560              : 
     561              : #[cfg(test)]
     562              : mod tests {
     563              :     use super::*;
     564              : 
     565           44 :     fn data(statements: u32) -> DataflowLayoutBox {
     566           44 :         DataflowLayoutBox { statements, ..Default::default() }
     567           44 :     }
     568              : 
     569           10 :     fn chart() -> DataflowLayoutBox {
     570           10 :         DataflowLayoutBox { statements: 1, chart_statement: Some(0), ..Default::default() }
     571           10 :     }
     572              : 
     573           33 :     fn edge(from_box: u32, from_statement: u32, to_box: u32, to_statement: u32) -> DataflowLayoutEdge {
     574           33 :         DataflowLayoutEdge { from_box, from_statement, to_box, to_statement }
     575           33 :     }
     576              : 
     577              :     /// The boxes with hints where a layout put them
     578           10 :     fn hinted(boxes: &[DataflowLayoutBox], geometry: &[DataflowBoxGeometry]) -> Vec<DataflowLayoutBox> {
     579           57 :         boxes.iter().zip(geometry).map(|(b, g)| DataflowLayoutBox { hint: Some((g.x, g.y)), ..b.clone() }).collect()
     580           10 :     }
     581              : 
     582              :     /// A source, two consumers of it, a chart of the first consumer, and a pair of boxes on their own
     583            5 :     fn two_parts() -> (Vec<DataflowLayoutBox>, Vec<DataflowLayoutEdge>) {
     584            5 :         let boxes = vec![data(2), data(1), data(1), chart(), data(1), data(1)];
     585            5 :         let edges = vec![edge(0, 0, 1, 0), edge(0, 1, 2, 0), edge(1, 0, 3, 0), edge(4, 0, 5, 0)];
     586            5 :         (boxes, edges)
     587            5 :     }
     588              : 
     589            3 :     fn assert_no_overlaps(geometry: &[DataflowBoxGeometry]) {
     590           15 :         for (i, a) in geometry.iter().enumerate() {
     591           33 :             for b in &geometry[i + 1..] {
     592           33 :                 let overlap = a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
     593           33 :                 assert!(!overlap, "{a:?} overlaps {b:?}");
     594              :             }
     595              :         }
     596            3 :     }
     597              : 
     598              :     #[test]
     599            1 :     fn box_geometry_stacks_rows_and_charts() {
     600            1 :         let config = DataflowLayoutConfig::default();
     601            1 :         let g = box_geometry(&DataflowLayoutBox { statements: 2, chart_statement: Some(0), ..Default::default() }, &config);
     602            1 :         assert_eq!(g.rows, vec![40.0, 40.0 + 32.0 + 8.0 + 200.0]);
     603            1 :         assert_eq!(g.height, 40.0 + 32.0 + 8.0 + 200.0 + 32.0 + 8.0);
     604              :         // A box with a chart is wider than a box without one
     605            1 :         assert_eq!(g.width, config.chart_box_width);
     606            1 :         assert_eq!(box_geometry(&DataflowLayoutBox { statements: 2, ..Default::default() }, &config).width, config.box_width);
     607              :         // A box with its own size, e.g. from a resized `@flow` tag
     608            1 :         let sized = DataflowLayoutBox { statements: 2, chart_statement: Some(0), width: Some(400.0), chart_height: Some(96.0), ..Default::default() };
     609            1 :         let sized = box_geometry(&sized, &config);
     610            1 :         assert_eq!(sized.width, 400.0);
     611            1 :         assert_eq!(sized.rows, vec![40.0, 40.0 + 32.0 + 8.0 + 96.0]);
     612            1 :         let collapsed = box_geometry(&DataflowLayoutBox { statements: 2, collapsed: true, ..Default::default() }, &config);
     613            1 :         assert_eq!(collapsed.height, 32.0);
     614            1 :         assert_eq!(collapsed.port(1, &config), 16.0);
     615            1 :     }
     616              : 
     617              :     #[test]
     618            1 :     fn flows_left_to_right_and_aligns_the_connected_rows() {
     619            1 :         let config = DataflowLayoutConfig::default();
     620            1 :         let boxes = [data(2), data(1), data(1)];
     621            1 :         let edges = [edge(0, 1, 1, 0), edge(1, 0, 2, 0)];
     622            1 :         let g = compute_dataflow_layout(&boxes, &edges, &config);
     623            1 :         assert!(g[0].x + g[0].width < g[1].x && g[1].x + g[1].width < g[2].x);
     624              :         // The row a box reads is level with the row it reads it from
     625            1 :         assert_eq!(g[0].y + g[0].port(1, &config), g[1].y + g[1].port(0, &config));
     626            1 :         assert_eq!(g[1].y + g[1].port(0, &config), g[2].y + g[2].port(0, &config));
     627            1 :     }
     628              : 
     629              :     #[test]
     630            1 :     fn survives_cycles() {
     631            1 :         let boxes = [data(1), data(1), data(1)];
     632            1 :         let edges = [edge(0, 0, 1, 0), edge(1, 0, 2, 0), edge(2, 0, 0, 0)];
     633            1 :         let (g, routes) = compute_dataflow_layout_with_routes(&boxes, &edges, &DataflowLayoutConfig::default());
     634            1 :         assert_no_overlaps(&g);
     635            1 :         assert_eq!(routes.len(), 3);
     636            1 :     }
     637              : 
     638              :     #[test]
     639            1 :     fn a_fan_out_stays_one_column() {
     640            1 :         let config = DataflowLayoutConfig::default();
     641            1 :         let boxes = [data(1), chart(), chart(), chart(), chart(), chart()];
     642            5 :         let edges: Vec<_> = (1..6).map(|i| edge(0, 0, i, 0)).collect();
     643            1 :         let g = compute_dataflow_layout(&boxes, &edges, &config);
     644            5 :         assert_eq!(g[1..].iter().map(|g| g.x as i64).collect::<BTreeSet<_>>().len(), 1);
     645              :         // In the order of the session, from top to bottom
     646            4 :         assert!(g[1..].windows(2).all(|w| w[0].y < w[1].y));
     647            1 :         assert_no_overlaps(&g);
     648            1 :     }
     649              : 
     650              :     #[test]
     651            1 :     fn boxes_without_edges_go_into_rows_below() {
     652            1 :         let config = DataflowLayoutConfig::default();
     653            1 :         let boxes = [data(1), data(1), data(1), data(1), data(1), data(1), data(1)];
     654            1 :         let g = compute_dataflow_layout(&boxes, &[edge(0, 0, 1, 0)], &config);
     655            1 :         let bottom = g[0].y.max(g[1].y) + 80.0;
     656            5 :         assert!(g[2..].iter().all(|b| b.y > bottom));
     657            1 :         assert_eq!(g[2].y, g[5].y);
     658            1 :         assert!(g[6].y > g[2].y);
     659            1 :         assert_eq!(g[6].x, g[2].x);
     660            1 :     }
     661              : 
     662              :     #[test]
     663            1 :     fn reload_reproduces_the_layout_and_its_routes() {
     664            1 :         let config = DataflowLayoutConfig::default();
     665            1 :         let (boxes, edges) = two_parts();
     666            1 :         let (first, routes) = compute_dataflow_layout_with_routes(&boxes, &edges, &config);
     667            1 :         assert_eq!(routes.len(), edges.len());
     668            1 :         let (reloaded, reloaded_routes) = compute_dataflow_layout_with_routes(&hinted(&boxes, &first), &edges, &config);
     669            1 :         assert_eq!(reloaded, first);
     670            1 :         assert_eq!(reloaded_routes, routes);
     671              :         // Orthogonal routes from the right border of the source row to the left border of the target row
     672            4 :         for ((from, fs, to, ts), points) in &routes {
     673            4 :             let (f, t) = (&first[*from as usize], &first[*to as usize]);
     674            4 :             assert_eq!(points.first(), Some(&(f.x + f.width, f.y + f.port(*fs, &config))));
     675            4 :             assert_eq!(points.last(), Some(&(t.x, t.y + t.port(*ts, &config))));
     676            6 :             assert!(points.windows(2).all(|w| w[0].0 == w[1].0 || w[0].1 == w[1].1));
     677              :         }
     678            1 :     }
     679              : 
     680              :     #[test]
     681            1 :     fn a_drop_keeps_the_y_and_the_column_and_leaves_the_others() {
     682            1 :         let config = DataflowLayoutConfig::default();
     683            1 :         let (boxes, edges) = two_parts();
     684            1 :         let first = compute_dataflow_layout(&boxes, &edges, &config);
     685              :         // The second consumer goes far down in its column
     686            1 :         let mut dropped = hinted(&boxes, &first);
     687            1 :         dropped[2].hint = Some((first[2].x + 20.0, first[2].y + 400.0));
     688            1 :         dropped[2].dropped = true;
     689            1 :         let after = compute_dataflow_layout(&dropped, &edges, &config);
     690            1 :         assert_eq!((after[2].x, after[2].y), (first[2].x, first[2].y + 400.0));
     691            5 :         for i in [0, 1, 3, 4, 5] {
     692            5 :             assert_eq!((after[i].x, after[i].y), (first[i].x, first[i].y), "box {i} moved");
     693              :         }
     694            1 :         assert_no_overlaps(&after);
     695              :         // The layout after the drop reloads as it is
     696            1 :         assert_eq!(compute_dataflow_layout(&hinted(&boxes, &after), &edges, &config), after);
     697            1 :     }
     698              : 
     699              :     #[test]
     700            1 :     fn a_drop_into_a_gap_makes_a_column() {
     701            1 :         let config = DataflowLayoutConfig::default();
     702            1 :         let (boxes, edges) = two_parts();
     703            1 :         let first = compute_dataflow_layout(&boxes, &edges, &config);
     704              :         // The chart of the first consumer is dropped with its center in the gap between the source and the consumers
     705            1 :         let mut dropped = hinted(&boxes, &first);
     706            1 :         let gap = (first[0].x + first[0].width + first[1].x) / 2.0;
     707            1 :         dropped[3].hint = Some((gap - first[3].width / 2.0, first[3].y));
     708            1 :         dropped[3].dropped = true;
     709            1 :         let after = compute_dataflow_layout(&dropped, &edges, &config);
     710              :         // The chart reads the first consumer, so its new column right of the source pushes the consumer right of it
     711            1 :         assert!(after[3].x > after[0].x + after[0].width);
     712            1 :         assert!(after[1].x > after[3].x + after[3].width);
     713              :         // The other part stays
     714            1 :         assert_eq!((after[4].x, after[4].y, after[5].x, after[5].y), (first[4].x, first[4].y, first[5].x, first[5].y));
     715            1 :     }
     716              : 
     717              :     #[test]
     718            1 :     fn a_box_without_a_hint_goes_next_to_its_producer() {
     719            1 :         let config = DataflowLayoutConfig::default();
     720            1 :         let (boxes, edges) = two_parts();
     721            1 :         let first = compute_dataflow_layout(&boxes, &edges, &config);
     722            1 :         let mut hints = hinted(&boxes, &first);
     723            1 :         hints[3].hint = None;
     724            1 :         let after = compute_dataflow_layout(&hints, &edges, &config);
     725            1 :         assert!(after[3].x > after[1].x + after[1].width);
     726              :         // Level with the row it reads
     727            1 :         assert_eq!(after[3].y + after[3].port(0, &config), after[1].y + after[1].port(0, &config));
     728            5 :         for i in [0, 1, 2, 4, 5] {
     729            5 :             assert_eq!((after[i].x, after[i].y), (first[i].x, first[i].y), "box {i} moved");
     730              :         }
     731            1 :     }
     732              : 
     733              :     #[test]
     734            1 :     fn rails_run_in_the_middle_of_the_gap_between_columns() {
     735            1 :         let config = DataflowLayoutConfig::default();
     736              :         // Two consumers of two rows, the lower one dropped further down: its edge needs a rail
     737            1 :         let boxes = [data(2), data(1), data(1)];
     738            1 :         let edges = [edge(0, 0, 1, 0), edge(0, 1, 2, 0)];
     739            1 :         let first = compute_dataflow_layout(&boxes, &edges, &config);
     740            1 :         let mut dropped = hinted(&boxes, &first);
     741            1 :         dropped[2].hint = Some((first[2].x, first[2].y + 200.0));
     742            1 :         dropped[2].dropped = true;
     743            1 :         let (g, routes) = compute_dataflow_layout_with_routes(&dropped, &edges, &config);
     744            1 :         let gap = (g[0].x + g[0].width, g[2].x);
     745            2 :         let rail = routes.iter().find(|(k, _)| k.2 == 2).unwrap().1.windows(2).find(|w| w[0].0 == w[1].0).unwrap()[0].0;
     746            1 :         assert!((rail - (gap.0 + gap.1) / 2.0).abs() <= 1.0, "rail at {rail} in {gap:?}");
     747            1 :     }
     748              : 
     749              :     #[test]
     750            1 :     fn a_drop_close_to_a_straight_edge_lines_up_the_rows() {
     751            1 :         let config = DataflowLayoutConfig::default();
     752            1 :         let (boxes, edges) = two_parts();
     753            1 :         let first = compute_dataflow_layout(&boxes, &edges, &config);
     754            4 :         let row = |g: &[DataflowBoxGeometry], b: usize, s: u32| g[b].y + g[b].port(s, &config);
     755              :         // The chart goes 400 down and an offset further, then its producer is dropped 400 down: the row of the
     756              :         // producer is the offset away from the row of the chart and lines up with it within `align_snap`
     757            2 :         for (offset, snaps) in [(10.0, true), (config.align_snap + 8.0, false)] {
     758            2 :             let mut dropped = hinted(&boxes, &first);
     759            2 :             dropped[3].hint = Some((first[3].x, first[3].y + 400.0 + offset));
     760            2 :             dropped[3].dropped = true;
     761            2 :             let moved = compute_dataflow_layout(&dropped, &edges, &config);
     762            2 :             let mut again = hinted(&boxes, &moved);
     763            2 :             again[1].hint = Some((moved[1].x, moved[1].y + 400.0));
     764            2 :             again[1].dropped = true;
     765            2 :             let after = compute_dataflow_layout(&again, &edges, &config);
     766            2 :             assert_eq!(row(&after, 1, 0) == row(&after, 3, 0), snaps, "offset {offset}");
     767              :         }
     768            1 :     }
     769              : 
     770              :     #[test]
     771            1 :     fn boxes_without_edges_keep_their_hints() {
     772            1 :         let config = DataflowLayoutConfig::default();
     773            1 :         let boxes = [DataflowLayoutBox { hint: Some((120.0, 40.0)), ..data(1) }, data(1)];
     774            1 :         let g = compute_dataflow_layout(&boxes, &[], &config);
     775            1 :         assert_eq!((g[0].x, g[0].y), (120.0, 40.0));
     776              :         // A box without a hint goes below, at their left edge
     777            1 :         assert_eq!(g[1].x, 120.0);
     778            1 :         assert!(g[1].y > 40.0 + g[0].height);
     779            1 :     }
     780              : }
        

Generated by: LCOV version 2.0-1