use crate::{
Component,
layout::{
Layout,
Rect,
},
renderer::Rendered,
};
#[derive(Debug, Clone, PartialEq, Default)]
pub enum Shadow {
#[default]
None,
Dim {
style: String,
},
Drop {
style: String,
offset_x: i16,
offset_y: i16,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum LayerKind {
#[default]
Base,
Overlay,
Modal,
}
#[derive(Default)]
pub struct Layer {
pub components: Vec<Box<dyn Component>>,
pub layout: Option<Layout>,
pub shadow: Shadow,
pub visible: bool,
pub kind: LayerKind,
}
impl Layer {
pub fn new() -> Self {
Self {
components: Vec::new(),
layout: None,
shadow: Shadow::None,
visible: true,
kind: LayerKind::Base,
}
}
pub fn with_component(component: Box<dyn Component>) -> Self {
Self {
components: vec![component],
layout: None,
shadow: Shadow::None,
visible: true,
kind: LayerKind::Base,
}
}
pub fn set_layout(&mut self, layout: Layout) {
self.layout = Some(layout);
}
pub fn mount(&mut self, component: Box<dyn Component>) {
self.components.push(component);
}
pub fn with_kind(mut self, kind: LayerKind) -> Self {
self.kind = kind;
self
}
pub fn render(&self, width: u16, height: u16, focused_index: Option<usize>) -> Rendered {
if !self.visible {
let mut rendered = Rendered::empty();
while rendered.lines.len() < height as usize {
rendered.lines.push(String::new());
}
return rendered;
}
let mut rendered = Rendered::empty();
let term_rect = Rect::new(0, 0, width, height);
if let Some(ref layout) = self.layout {
let areas = layout.split(term_rect);
for (i, (child, area)) in self.components.iter().zip(areas.iter()).enumerate() {
let child_rendered = match child.render_rect(*area) {
| Ok(r) => r,
| Err(_) => continue,
};
child_rendered.blit_into_rect(&mut rendered, *area);
if Some(i) == focused_index &&
let Some((r_local, c_local)) = child_rendered.cursor
{
rendered.cursor = Some((area.y as usize + r_local, area.x as usize + c_local));
}
}
} else {
let mut row = 0usize;
for (i, child) in self.components.iter().enumerate() {
if row >= height as usize {
break;
}
let child_rendered = match child.render(width) {
| Ok(r) => r,
| Err(_) => continue,
};
let start_row = row;
for line in &child_rendered.lines {
if row < height as usize {
rendered.lines.push(line.clone());
row += 1;
}
}
if Some(i) == focused_index &&
let Some((r_local, c_local)) = child_rendered.cursor
{
rendered.cursor = Some((start_row + r_local, c_local));
}
for image in &child_rendered.images {
rendered.images.push(crate::renderer::ImageCommand {
id: image.id,
data: image.data.clone(),
row: start_row as u16 + image.row,
col: image.col,
});
}
}
}
while rendered.lines.len() < height as usize {
rendered.lines.push(String::new());
}
rendered
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
RenderError,
components::Text,
layout::{
Constraint,
Layout,
},
renderer::ImageCommand,
};
struct CursorComponent;
impl Component for CursorComponent {
fn render(&self, _width: u16) -> Result<Rendered, RenderError> {
Ok(Rendered {
lines: vec!["cursor".into()],
cursor: Some((0, 3)),
images: Vec::new(),
})
}
}
#[test]
fn layer_new_is_empty() {
let layer = Layer::new();
assert!(layer.components.is_empty());
assert!(layer.layout.is_none());
assert_eq!(layer.shadow, Shadow::None);
assert!(layer.visible);
}
#[test]
fn layer_render_empty_pads_to_terminal_size() {
let layer = Layer::new();
let rendered = layer.render(80, 24, None);
assert_eq!(rendered.lines.len(), 24);
assert!(rendered.cursor.is_none());
assert!(rendered.images.is_empty());
}
#[test]
fn layer_render_single_component_vertical_stack() {
let mut layer = Layer::new();
layer.mount(Box::new(Text::new("hello", 0, 0)));
let rendered = layer.render(80, 24, None);
assert!(rendered.lines[0].starts_with("hello"));
assert_eq!(crate::utils::visible_width(&rendered.lines[0]), 80);
assert_eq!(rendered.lines.len(), 24);
}
#[test]
fn layer_render_multiple_components_vertical_stack() {
let mut layer = Layer::new();
layer.mount(Box::new(Text::new("first", 0, 0)));
layer.mount(Box::new(Text::new("second", 0, 0)));
let rendered = layer.render(80, 24, None);
assert!(rendered.lines[0].starts_with("first"));
assert!(rendered.lines[1].starts_with("second"));
assert_eq!(rendered.lines.len(), 24);
}
#[test]
fn layer_render_clips_component_to_size() {
let mut layer = Layer::new();
layer.mount(Box::new(Text::new("hello", 0, 0)));
let rendered = layer.render(4, 2, None);
assert!(rendered.lines[0].starts_with("hel"));
assert_eq!(rendered.lines.len(), 2);
}
#[test]
fn layer_render_focused_component_cursor() {
let mut layer = Layer::new();
layer.mount(Box::new(Text::new("a", 0, 0)));
layer.mount(Box::new(CursorComponent));
let rendered = layer.render(80, 24, Some(1));
assert_eq!(rendered.cursor, Some((1, 3)));
}
#[test]
fn layer_render_preserves_images() {
struct ImageComponent;
impl Component for ImageComponent {
fn render(&self, _width: u16) -> Result<Rendered, RenderError> {
Ok(Rendered {
lines: vec!["img".into()],
cursor: None,
images: vec![ImageCommand {
id: 7,
data: "data".into(),
row: 0,
col: 0,
}],
})
}
}
let mut layer = Layer::new();
layer.mount(Box::new(ImageComponent));
let rendered = layer.render(80, 24, None);
assert_eq!(rendered.images.len(), 1);
assert_eq!(rendered.images[0].id, 7);
}
#[test]
fn layer_shadow_variants_clone_and_equal() {
let dim = Shadow::Dim {
style: "\x1b[2m".into(),
};
let cloned = dim.clone();
assert_eq!(dim, cloned);
let drop_shadow = Shadow::Drop {
style: "\x1b[2m".into(),
offset_x: 1,
offset_y: 1,
};
assert_ne!(dim, drop_shadow);
}
#[test]
fn layer_visible_false_skips_render() {
let mut layer = Layer::new();
layer.mount(Box::new(Text::new("hello", 0, 0)));
layer.visible = false;
let rendered = layer.render(80, 24, None);
assert_eq!(rendered.lines[0], "");
assert_eq!(rendered.lines.len(), 24);
}
#[test]
fn shadow_default_is_none() {
assert_eq!(Shadow::default(), Shadow::None);
}
#[test]
fn layer_kind_default_is_base() {
assert_eq!(LayerKind::default(), LayerKind::Base);
}
#[test]
fn layer_with_component() {
let layer = Layer::with_component(Box::new(Text::new("hi", 0, 0)));
assert_eq!(layer.components.len(), 1);
assert!(layer.layout.is_none());
assert_eq!(layer.shadow, Shadow::None);
assert_eq!(layer.kind, LayerKind::Base);
assert!(layer.visible);
}
#[test]
fn layer_with_kind() {
let layer = Layer::new().with_kind(LayerKind::Overlay);
assert_eq!(layer.kind, LayerKind::Overlay);
}
#[test]
fn layer_render_with_layout_focuses_cursor() {
let mut layer = Layer::new();
layer.set_layout(Layout::horizontal([
Constraint::Percentage(50),
Constraint::Percentage(50),
]));
layer.mount(Box::new(Text::new("a", 0, 0)));
layer.mount(Box::new(CursorComponent));
let rendered = layer.render(80, 24, Some(1));
assert!(rendered.cursor.is_some());
let (_, col) = rendered.cursor.unwrap();
assert!(col >= 40, "expected cursor in second half, got col {}", col);
}
#[test]
fn layer_render_clips_to_height() {
let mut layer = Layer::new();
layer.mount(Box::new(Text::new("a", 0, 0)));
layer.mount(Box::new(Text::new("b", 0, 0)));
layer.mount(Box::new(Text::new("c", 0, 0)));
layer.mount(Box::new(Text::new("d", 0, 0)));
let rendered = layer.render(80, 2, None);
assert_eq!(rendered.lines.len(), 2);
assert!(rendered.lines[0].starts_with("a"));
assert!(rendered.lines[1].starts_with("b"));
}
#[test]
fn layer_render_ignores_failing_component() {
struct Fail;
impl Component for Fail {
fn render(&self, _width: u16) -> Result<Rendered, RenderError> {
Err(RenderError::WidthOverflow {
line: String::new(),
width: 0,
actual: 0,
})
}
}
let mut layer = Layer::new();
layer.mount(Box::new(Text::new("ok", 0, 0)));
layer.mount(Box::new(Fail));
let rendered = layer.render(80, 24, None);
assert!(rendered.lines[0].starts_with("ok"));
}
}