Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 0 additions & 69 deletions eden/scm/lib/renderdag/src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,75 +22,6 @@ pub struct OutputRendererOptions {
pub graph_row_shape_style: crate::pipeline::types::GraphRowShapeStyle,
}

/// Common stateful string line output utilities shared by ASCII and box-drawing
/// renderers.
#[derive(Default)]
pub(crate) struct OutputRendererState {
queued_pad_line: Option<String>,
/// If > 1, no need for an extra separator for the next row.
/// Separator is only needed for adjacent single-line node lines.
/// If there are link, pad, term, or other kinds of node lines,
/// no need to draw separator.
row_height: usize,
}

impl OutputRendererState {
pub(crate) fn begin_row(&mut self, out: &mut String, separator_line: bool) {
self.flush_queued_pad_line(out);
if separator_line && self.row_height == 1 {
self.push_line(out, "");
}
self.row_height = 0;
}

pub(crate) fn push_line_with_message(
&mut self,
out: &mut String,
mut line: String,
message: Option<&str>,
) {
if let Some(message) = message {
line.push(' ');
line.push_str(message);
}
self.push_line(out, &line);
}

pub(crate) fn push_pad_lines<'a>(
&mut self,
out: &mut String,
base_pad_line: &str,
message_lines: impl Iterator<Item = &'a str>,
) -> bool {
let mut emitted = false;
for message in message_lines {
let mut pad_line = String::with_capacity(base_pad_line.len() + message.len() + 1);
pad_line.push_str(base_pad_line);
pad_line.push(' ');
pad_line.push_str(message);
self.push_line(out, &pad_line);
emitted = true;
}
emitted
}

pub(crate) fn queue_pad_line(&mut self, pad_line: String) {
self.queued_pad_line = Some(pad_line);
}

fn push_line(&mut self, out: &mut String, line: &str) {
out.push_str(line.trim_end());
out.push('\n');
self.row_height = self.row_height.saturating_add(1);
}

fn flush_queued_pad_line(&mut self, out: &mut String) {
if let Some(pad_line) = self.queued_pad_line.take() {
self.push_line(out, &pad_line);
}
}
}

pub struct OutputRendererBuilder<N, R>
where
R: Renderer<N, Output = GraphRow<N>> + Sized,
Expand Down
3 changes: 2 additions & 1 deletion eden/scm/lib/renderdag/src/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ mod tests {
.parts
.iter()
.map(|part| match part {
PrefixLinePart::Text(text) => text.as_str(),
PrefixLinePart::Text(text)
| PrefixLinePart::ColumnText { text, .. } => text.as_str(),
PrefixLinePart::NodeGlyph => "o",
})
.collect();
Expand Down
151 changes: 130 additions & 21 deletions eden/scm/lib/renderdag/src/pipeline/prefix_lines_to_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,25 @@
* LICENSE file in the root directory of this source tree.
*/

use crate::output::OutputRendererState;
use crate::pad::pad_lines;
use crate::pipeline::types::LinePiece;
use crate::pipeline::types::LinkLinePlacement;
use crate::pipeline::types::PrefixLine;
use crate::pipeline::types::PrefixLineKind;
use crate::pipeline::types::PrefixLinePart;

/// Stateful renderer for the final pipeline stage.
///
/// It fills node glyph slots, attaches message lines, and writes complete text
/// lines for one rendered graph row at a time.
/// It fills node glyph slots, attaches message lines, and writes complete text lines for one
/// rendered graph row at a time.
#[derive(Default)]
pub struct PrefixLinesToText {
state: OutputRendererState,
/// The queued pad line, and the node glyph to render it with.
queued_pad_line: Option<(PrefixLine, String)>,
/// If > 1, no need for an extra separator for the next row. Separator is only needed for
/// adjacent single-line node lines. If there are link, pad, term, or other kinds of node lines,
/// no need to draw separator.
row_height: usize,
link_line_placement: LinkLinePlacement,
}

Expand Down Expand Up @@ -65,6 +70,35 @@ impl PrefixLinesToText {
message: &str,
min_row_height: usize,
) {
self.write_next_lines(
|line| {
for piece in line {
out.push_str(piece.text);
}
out.push('\n');
},
prefix_lines,
separator_line,
glyph,
message,
min_row_height,
);
}

/// Give each line of the next row to `out`, as a list of pieces.
///
/// A caller can use this to style each piece, for example to give each column its own color.
/// The last piece of a line has no trailing whitespace.
pub fn write_next_lines(
&mut self,
mut out: impl FnMut(&[LinePiece<'_>]),
prefix_lines: Vec<PrefixLine>,
separator_line: bool,
glyph: &str,
message: &str,
min_row_height: usize,
) {
let out = &mut out;
let mut message_lines = pad_lines(message.lines(), min_row_height);
let extra_message_lines = if self.link_line_placement == LinkLinePlacement::Bottom {
let fixed_lines = prefix_lines
Expand All @@ -82,7 +116,7 @@ impl PrefixLinesToText {
let mut repeatable_line = None;
let mut has_term_row = false;

self.state.begin_row(out, separator_line);
self.begin_row(out, separator_line);

for prefix_line in prefix_lines {
// Text starts at the node; extra message lines precede links only for Bottom.
Expand All @@ -91,9 +125,9 @@ impl PrefixLinesToText {
}
if prefix_line.kind == PrefixLineKind::PostNode {
if extra_message_lines > 0 {
let base_line = render_prefix_line(&prefix_line, glyph);
let base_line = line_pieces(&prefix_line, glyph);
// Reserve message lines for the mandatory Link, Term, and Ancestry lines.
self.state.push_pad_lines(
self.push_pad_lines(
out,
&base_line,
message_lines.by_ref().take(extra_message_lines),
Expand All @@ -106,27 +140,102 @@ impl PrefixLinesToText {
continue;
}
has_term_row |= prefix_line.kind == PrefixLineKind::Term;
let text = render_prefix_line(&prefix_line, glyph);
self.state
.push_line_with_message(out, text, message_lines.next());
let line = line_pieces(&prefix_line, glyph);
self.push_line_with_message(out, line, message_lines.next());
}

if let Some(repeatable_line) = repeatable_line {
let base_line = render_prefix_line(&repeatable_line, glyph);
if !self.state.push_pad_lines(out, &base_line, message_lines) && has_term_row {
self.state.queue_pad_line(base_line);
let base_line = line_pieces(&repeatable_line, glyph);
if !self.push_pad_lines(out, &base_line, message_lines) && has_term_row {
self.queue_pad_line(repeatable_line, glyph);
}
}
}
}

fn render_prefix_line(prefix_line: &PrefixLine, glyph: &str) -> String {
let mut out = String::new();
for part in prefix_line.parts.iter() {
match part {
PrefixLinePart::Text(text) => out.push_str(text),
PrefixLinePart::NodeGlyph => out.push_str(glyph),
fn begin_row(&mut self, out: &mut impl FnMut(&[LinePiece<'_>]), separator_line: bool) {
self.flush_queued_pad_line(out);
if separator_line && self.row_height == 1 {
self.push_line(out, &mut []);
}
self.row_height = 0;
}
out

fn push_line_with_message<'a>(
&mut self,
out: &mut impl FnMut(&[LinePiece<'_>]),
mut line: Vec<LinePiece<'a>>,
message: Option<&'a str>,
) {
if let Some(message) = message {
line.push(LinePiece {
column: None,
text: " ",
});
line.push(LinePiece {
column: None,
text: message,
});
}
self.push_line(out, &mut line);
}

fn push_pad_lines<'a>(
&mut self,
out: &mut impl FnMut(&[LinePiece<'_>]),
base_pad_line: &[LinePiece<'_>],
message_lines: impl Iterator<Item = &'a str>,
) -> bool {
let mut emitted = false;
for message in message_lines {
self.push_line_with_message(out, base_pad_line.to_vec(), Some(message));
emitted = true;
}
emitted
}

fn queue_pad_line(&mut self, pad_line: PrefixLine, glyph: &str) {
self.queued_pad_line = Some((pad_line, glyph.to_owned()));
}

fn push_line(&mut self, out: &mut impl FnMut(&[LinePiece<'_>]), line: &mut [LinePiece<'_>]) {
out(trim_end(line));
self.row_height = self.row_height.saturating_add(1);
}

fn flush_queued_pad_line(&mut self, out: &mut impl FnMut(&[LinePiece<'_>])) {
if let Some((pad_line, glyph)) = self.queued_pad_line.take() {
self.push_line(out, &mut line_pieces(&pad_line, &glyph));
}
}
}

/// Remove trailing whitespace from a line, across pieces.
fn trim_end<'s, 'a>(line: &'s mut [LinePiece<'a>]) -> &'s [LinePiece<'a>] {
let end = line
.iter_mut()
.rposition(|piece| {
piece.text = piece.text.trim_end();
!piece.text.is_empty()
})
.map_or(0, |index| index + 1);
&line[..end]
}

/// Convert a prefix line into line pieces, with `glyph` in the node glyph slot.
fn line_pieces<'a>(prefix_line: &'a PrefixLine, glyph: &'a str) -> Vec<LinePiece<'a>> {
prefix_line
.parts
.iter()
.map(|part| match part {
PrefixLinePart::Text(text) => LinePiece { column: None, text },
PrefixLinePart::ColumnText { column, text } => LinePiece {
column: Some(*column),
text,
},
PrefixLinePart::NodeGlyph => LinePiece {
column: None,
text: glyph,
},
})
.collect()
}
97 changes: 97 additions & 0 deletions eden/scm/lib/renderdag/src/pipeline/row_shape_to_prefix_lines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ pub mod box_drawing;

use crate::pipeline::types::DashStyle;
use crate::pipeline::types::GraphRowShape;
use crate::pipeline::types::LinkLine;
use crate::pipeline::types::PadLine;
use crate::pipeline::types::PrefixLine;
use crate::pipeline::types::PrefixLineKind;
use crate::pipeline::types::PrefixLinePart;

fn ancestry_lines<N>(
row: &GraphRowShape<N>,
Expand All @@ -36,3 +39,97 @@ fn ancestry_lines<N>(
(kind, columns)
})
}

/// True if a link line cell only carries a horizontal line past its column.
fn is_horizontal_run(cell: LinkLine) -> bool {
cell.intersects(LinkLine::HORIZONTAL)
&& !cell.intersects(LinkLine::CHILD | LinkLine::ANY_FORK_OR_MERGE)
}

/// Find the owning column of each link line connector, scanning each horizontal run only once.
///
/// A connector joins two columns. It belongs to the column whose line ends in this link line. If
/// neither line or both lines end here, it belongs to the column that is farther from the node,
/// which is the branch that forks or merges.
fn connector_columns(
node_column: usize,
link_row: &[LinkLine],
) -> impl Iterator<Item = usize> + '_ {
let mut right = 0;
let mut owner = 0;
(0..link_row.len()).map(move |segment| {
if segment + 1 == link_row.len() {
return segment;
}
// Every connector before the right endpoint has the same owner.
if right <= segment {
let left = segment;
right = segment + 1;
while right + 1 < link_row.len() && is_horizontal_run(link_row[right]) {
right += 1;
}
owner = match (
link_row[left].intersects(LinkLine::VERTICAL),
link_row[right].intersects(LinkLine::VERTICAL),
) {
(true, false) => right,
(false, true) => left,
_ if node_column <= left => right,
_ => left,
};
}
owner
})
}

/// Make a part for graph text that draws part of the edge in `column`.
fn column_part(column: usize, text: impl Into<String>) -> PrefixLinePart {
let text = text.into();
if text.trim().is_empty() {
PrefixLinePart::Text(text)
} else {
PrefixLinePart::ColumnText { column, text }
}
}

/// Append graph text, reusing the last part when its column ownership matches.
#[inline]
fn push_column_part(parts: &mut Vec<PrefixLinePart>, column: usize, text: &str) {
let column = (!text.trim().is_empty()).then_some(column);
match (parts.last_mut(), column) {
(Some(PrefixLinePart::Text(last)), None) => last.push_str(text),
(
Some(PrefixLinePart::ColumnText {
column: last_column,
text: last,
}),
Some(column),
) if *last_column == column => last.push_str(text),
(_, None) => parts.push(PrefixLinePart::Text(text.to_owned())),
(_, Some(column)) => parts.push(PrefixLinePart::ColumnText {
column,
text: text.to_owned(),
}),
}
}

/// Render one cell per column, coalescing adjacent parts with the same ownership.
#[inline]
fn column_line<'a>(kind: PrefixLineKind, cells: impl IntoIterator<Item = &'a str>) -> PrefixLine {
let cells = cells.into_iter();
let mut parts = Vec::with_capacity(cells.size_hint().0.min(4));
for (column, text) in cells.enumerate() {
push_column_part(&mut parts, column, text);
}
PrefixLine { parts, kind }
}

/// Make the parts for graph text that has one character for each of `columns`.
fn column_parts(
text: &str,
columns: impl IntoIterator<Item = usize>,
) -> impl Iterator<Item = PrefixLinePart> {
text.chars()
.zip(columns)
.map(|(c, column)| column_part(column, c))
}
Loading