use crate::compositor::Layer;
use crate::compositor::chop::chop_segments;
use crate::compositor::cuts::find_cuts;
use crate::compositor::zorder::select_topmost;
use crate::segment::Segment;
pub fn compose_line(layers: &[Layer], row: u16, screen_width: u16) -> Vec<Segment> {
let mut result = Vec::new();
let cuts = find_cuts(layers, row, screen_width);
if cuts.len() <= 1 {
if screen_width > 0 {
result.push(Segment::new(" ".repeat(screen_width as usize)));
}
return result;
}
for i in 0..cuts.len() - 1 {
let x_start = cuts[i];
let x_end = cuts[i + 1];
let width = x_end - x_start;
if width == 0 {
continue;
}
match select_topmost(layers, row, x_start, x_end) {
Some(layer_idx) => {
let layer = &layers[layer_idx];
match layer.line_for_row(row) {
Some(line_segments) => {
let chopped =
chop_segments(line_segments, layer.region.position.x, x_start, width);
result.extend(chopped);
}
None => {
result.push(Segment::new(" ".repeat(width as usize)));
}
}
}
None => {
result.push(Segment::new(" ".repeat(width as usize)));
}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::Rect;
#[test]
fn single_layer_full_width() {
let layer = Layer::new(
1,
Rect::new(0, 0, 80, 10),
0,
vec![vec![Segment::new("Hello, world!")]],
);
let layers = vec![layer];
let segments = compose_line(&layers, 0, 80);
let found_text = segments.iter().any(|s| s.text.contains("Hello, world!"));
assert!(found_text);
}
#[test]
fn two_layers_side_by_side() {
let layer1 = Layer::new(
1,
Rect::new(0, 0, 40, 10),
0,
vec![vec![Segment::new("Left")]],
);
let layer2 = Layer::new(
2,
Rect::new(40, 0, 40, 10),
0,
vec![vec![Segment::new("Right")]],
);
let layers = vec![layer1, layer2];
let segments = compose_line(&layers, 0, 80);
assert!(segments.len() >= 2);
let found_left = segments.iter().any(|s| s.text.contains("Left"));
assert!(found_left);
let found_right = segments.iter().any(|s| s.text.contains("Right"));
assert!(found_right);
}
#[test]
fn overlapping_layers_topmost_wins() {
let layer1 = Layer::new(
1,
Rect::new(0, 0, 80, 10),
0,
vec![vec![Segment::new("Background")]],
);
let layer2 = Layer::new(
2,
Rect::new(10, 0, 20, 10),
10,
vec![vec![Segment::new("Overlay")]],
);
let layers = vec![layer1, layer2];
let segments = compose_line(&layers, 0, 80);
let found_overlay = segments.iter().any(|s| s.text.contains("Overlay"));
assert!(found_overlay);
}
#[test]
fn gap_between_layers_filled_with_blank() {
let layer1 = Layer::new(1, Rect::new(0, 0, 10, 10), 0, vec![vec![Segment::new("A")]]);
let layer2 = Layer::new(
2,
Rect::new(30, 0, 10, 10),
0,
vec![vec![Segment::new("B")]],
);
let layers = vec![layer1, layer2];
let segments = compose_line(&layers, 0, 80);
assert!(segments.len() >= 3);
let has_blank = segments.iter().any(|s| s.text.trim().is_empty());
assert!(has_blank);
}
#[test]
fn layer_extends_beyond_screen_clipped() {
let layer = Layer::new(
1,
Rect::new(70, 0, 20, 10),
0,
vec![vec![Segment::new("Very long text that exceeds screen")]],
);
let layers = vec![layer];
let segments = compose_line(&layers, 0, 80);
let total_width: usize = segments.iter().map(|s| s.width()).sum();
assert!(total_width <= 80);
}
#[test]
fn empty_row_no_layers() {
let layers: Vec<Layer> = vec![];
let segments = compose_line(&layers, 0, 80);
assert!(segments.len() == 1);
assert!(segments[0].text.trim().is_empty());
assert!(segments[0].width() == 80);
}
#[test]
fn layer_on_different_row_ignored() {
let layer = Layer::new(
1,
Rect::new(0, 10, 80, 5),
0,
vec![vec![Segment::new("Not on row 0")]],
);
let layers = vec![layer];
let segments = compose_line(&layers, 0, 80);
assert!(segments.len() == 1);
assert!(segments[0].text.trim().is_empty());
}
#[test]
fn zero_width_screen() {
let layer = Layer::new(1, Rect::new(0, 0, 10, 10), 0, vec![vec![Segment::new("X")]]);
let layers = vec![layer];
let segments = compose_line(&layers, 0, 0);
assert!(segments.is_empty());
}
#[test]
fn styled_segment_preserved() {
use crate::color::{Color, NamedColor};
use crate::style::Style;
let style = Style {
fg: Some(Color::Named(NamedColor::Red)),
..Default::default()
};
let mut seg = Segment::new("Styled");
seg.style = style.clone();
let layer = Layer::new(1, Rect::new(0, 0, 20, 10), 0, vec![vec![seg.clone()]]);
let layers = vec![layer];
let segments = compose_line(&layers, 0, 80);
let found_styled = segments.iter().any(|s| {
s.text.contains("Styled")
&& s.style.fg.is_some()
&& matches!(s.style.fg, Some(Color::Named(NamedColor::Red)))
});
assert!(found_styled);
}
#[test]
fn multiple_segments_in_layer() {
let layer = Layer::new(
1,
Rect::new(0, 0, 80, 10),
0,
vec![vec![
Segment::new("Hello "),
Segment::new("world"),
Segment::new("!"),
]],
);
let layers = vec![layer];
let segments = compose_line(&layers, 0, 80);
let combined: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(combined.contains("Hello"));
assert!(combined.contains("world"));
}
#[test]
fn three_overlapping_layers_z_order() {
let layer1 = Layer::new(
1,
Rect::new(0, 0, 80, 10),
0,
vec![vec![Segment::new("Bottom")]],
);
let layer2 = Layer::new(
2,
Rect::new(10, 0, 60, 10),
5,
vec![vec![Segment::new("Middle")]],
);
let layer3 = Layer::new(
3,
Rect::new(20, 0, 40, 10),
10,
vec![vec![Segment::new("Top")]],
);
let layers = vec![layer1, layer2, layer3];
let segments = compose_line(&layers, 0, 80);
let found_top = segments.iter().any(|s| s.text.contains("Top"));
assert!(found_top);
}
#[test]
fn compose_cjk_text_at_overlap_boundary() {
let layer1 = Layer::new(
1,
Rect::new(0, 0, 20, 1),
0,
vec![vec![Segment::new("\u{4e16}\u{754c}\u{4eba}")]],
);
let layer2 = Layer::new(
2,
Rect::new(3, 0, 5, 1),
10,
vec![vec![Segment::new("XXXXX")]],
);
let layers = vec![layer1, layer2];
let segments = compose_line(&layers, 0, 20);
let total_width: usize = segments.iter().map(|s| s.width()).sum();
assert_eq!(total_width, 20);
let combined: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(combined.contains("XXXXX"));
}
#[test]
fn compose_combining_marks_at_layer_boundary() {
let layer1 = Layer::new(
1,
Rect::new(0, 0, 20, 1),
0,
vec![vec![Segment::new("ae\u{0301}bc")]],
);
let layers = vec![layer1];
let segments = compose_line(&layers, 0, 20);
let combined: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(combined.contains("e\u{0301}") || combined.contains('a'));
let total_width: usize = segments.iter().map(|s| s.width()).sum();
assert_eq!(total_width, 20);
}
#[test]
fn compose_empty_segments_in_layers() {
let layer = Layer::new(
1,
Rect::new(0, 0, 20, 1),
0,
vec![vec![
Segment::new(""),
Segment::new("Hello"),
Segment::new(""),
Segment::new(" World"),
]],
);
let layers = vec![layer];
let segments = compose_line(&layers, 0, 20);
let combined: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(combined.contains("Hello"));
assert!(combined.contains("World"));
let total_width: usize = segments.iter().map(|s| s.width()).sum();
assert_eq!(total_width, 20);
}
#[test]
fn compose_very_long_grapheme_clusters() {
let emoji = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}";
let text = format!("A{}B", emoji);
let layer = Layer::new(1, Rect::new(0, 0, 20, 1), 0, vec![vec![Segment::new(text)]]);
let layers = vec![layer];
let segments = compose_line(&layers, 0, 20);
let total_width: usize = segments.iter().map(|s| s.width()).sum();
assert_eq!(total_width, 20);
let combined: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(combined.contains('A'));
assert!(combined.contains('B'));
}
}