use std::sync::{Arc, Mutex};
use crate::align::{Align, VerticalAlign};
use crate::cells::cell_len;
use crate::console::{Console, ConsoleOptions};
use crate::highlighter::ReprHighlighter;
use crate::measure::Measurement;
use crate::panel::Panel;
use crate::protocol::{Highlighter, Renderable};
use crate::ratio::{ratio_resolve, Edge};
use crate::region::Region;
use crate::segment::Segment;
use crate::style::Style;
use crate::table::{Cell, Table};
use crate::text::Text;
use crate::tree::Tree;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Splitter {
Row,
#[default]
Column,
}
impl Splitter {
pub fn name(self) -> &'static str {
match self {
Splitter::Row => "row",
Splitter::Column => "column",
}
}
pub fn tree_icon(self) -> &'static str {
match self {
Splitter::Row => "[layout.tree.row]⬌",
Splitter::Column => "[layout.tree.column]⬍",
}
}
pub fn from_name(name: &str) -> Option<Splitter> {
match name {
"row" => Some(Splitter::Row),
"column" => Some(Splitter::Column),
_ => None,
}
}
fn divide(self, children: &[&Layout], region: Region) -> Vec<Region> {
let edges: Vec<Edge> = children.iter().map(|child| child.edge()).collect();
let Region {
x,
y,
width,
height,
} = region;
let mut offset = 0;
match self {
Splitter::Row => ratio_resolve(width, &edges)
.into_iter()
.map(|child_width| {
let region = Region::new(x.saturating_add(offset), y, child_width, height);
offset = offset.saturating_add(child_width);
region
})
.collect(),
Splitter::Column => ratio_resolve(height, &edges)
.into_iter()
.map(|child_height| {
let region = Region::new(x, y.saturating_add(offset), width, child_height);
offset = offset.saturating_add(child_height);
region
})
.collect(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct LayoutRender {
pub name: Option<String>,
pub path: Vec<usize>,
pub region: Region,
pub render: Vec<Vec<Segment>>,
}
pub struct Layout {
renderable: Option<Box<dyn Renderable>>,
name: Option<String>,
visible: bool,
children: Vec<Layout>,
splitter: Splitter,
size: Option<usize>,
ratio: usize,
minimum_size: usize,
render_map: Mutex<Vec<LayoutRender>>,
}
impl Default for Layout {
fn default() -> Self {
Layout::new()
}
}
impl Layout {
pub fn new() -> Self {
Layout {
renderable: None,
name: None,
visible: true,
children: Vec::new(),
splitter: Splitter::Column,
size: None,
ratio: 1,
minimum_size: 1,
render_map: Mutex::new(Vec::new()),
}
}
pub fn with_renderable(renderable: Box<dyn Renderable>) -> Self {
let mut layout = Layout::new();
layout.renderable = Some(renderable);
layout
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn visible(mut self, visible: bool) -> Self {
self.visible = visible;
self
}
pub fn size(mut self, size: usize) -> Self {
self.size = Some(size);
self
}
pub fn ratio(mut self, ratio: usize) -> Self {
self.ratio = ratio;
self
}
pub fn minimum_size(mut self, minimum_size: usize) -> Self {
self.minimum_size = minimum_size;
self
}
pub fn set_name(&mut self, name: Option<String>) -> &mut Self {
self.name = name;
self
}
pub fn set_visible(&mut self, visible: bool) -> &mut Self {
self.visible = visible;
self
}
pub fn set_size(&mut self, size: Option<usize>) -> &mut Self {
self.size = size;
self
}
pub fn set_ratio(&mut self, ratio: usize) -> &mut Self {
self.ratio = ratio;
self
}
pub fn set_minimum_size(&mut self, minimum_size: usize) -> &mut Self {
self.minimum_size = minimum_size;
self
}
pub fn get_name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn is_visible(&self) -> bool {
self.visible
}
pub fn get_size(&self) -> Option<usize> {
self.size
}
pub fn get_ratio(&self) -> usize {
self.ratio
}
pub fn get_minimum_size(&self) -> usize {
self.minimum_size
}
pub fn splitter(&self) -> Splitter {
self.splitter
}
pub fn renderable(&self) -> Option<&dyn Renderable> {
self.renderable.as_deref()
}
pub fn update(&mut self, renderable: Box<dyn Renderable>) {
self.renderable = Some(renderable);
}
pub fn children(&self) -> Vec<&Layout> {
self.children.iter().filter(|child| child.visible).collect()
}
pub fn all_children(&self) -> &[Layout] {
&self.children
}
pub fn all_children_mut(&mut self) -> &mut Vec<Layout> {
&mut self.children
}
pub fn split(&mut self, children: Vec<Layout>, splitter: Splitter) {
self.splitter = splitter;
self.children = children;
}
pub fn split_column(&mut self, children: Vec<Layout>) {
self.split(children, Splitter::Column);
}
pub fn split_row(&mut self, children: Vec<Layout>) {
self.split(children, Splitter::Row);
}
pub fn add_split(&mut self, children: Vec<Layout>) {
self.children.extend(children);
}
pub fn unsplit(&mut self) {
self.children.clear();
}
pub fn get(&self, name: &str) -> Option<&Layout> {
if self.name.as_deref() == Some(name) {
return Some(self);
}
self.children.iter().find_map(|child| child.get(name))
}
pub fn get_mut(&mut self, name: &str) -> Option<&mut Layout> {
if self.name.as_deref() == Some(name) {
return Some(self);
}
self.children
.iter_mut()
.find_map(|child| child.get_mut(name))
}
pub fn at_path(&self, path: &[usize]) -> Option<&Layout> {
path.iter()
.try_fold(self, |layout, &index| layout.children.get(index))
}
fn edge(&self) -> Edge {
Edge::new(self.size, self.ratio, self.minimum_size)
}
fn repr_fields(&self) -> Vec<String> {
let mut fields = Vec::new();
if let Some(name) = &self.name {
fields.push(format!("name={}", py_repr_str(name)));
}
if let Some(size) = self.size {
fields.push(format!("size={size}"));
}
if self.minimum_size != 1 {
fields.push(format!("minimum_size={}", self.minimum_size));
}
if self.ratio != 1 {
fields.push(format!("ratio={}", self.ratio));
}
fields
}
pub fn tree(&self) -> Tree {
fn summary(layout: &Layout) -> Cell {
let mut table = Table::grid().padding(0, 1, 0, 0);
table.add_column("").add_column("");
table.add_row_cells(vec![
Cell::Markup(layout.splitter.tree_icon().to_string()),
Cell::Renderable(Arc::new(LayoutRepr {
fields: layout.repr_fields(),
dim: !layout.visible,
})),
]);
Cell::Renderable(Arc::new(table))
}
fn recurse(tree: &mut Tree, layout: &Layout) {
for child in &layout.children {
let node = tree.add(summary(child));
node.set_guide_style(format!("layout.tree.{}", child.splitter.name()));
recurse(node, child);
}
}
let mut tree = Tree::new(summary(self))
.guide_style(format!("layout.tree.{}", self.splitter.name()))
.highlight(true);
recurse(&mut tree, self);
tree
}
pub fn region_map(&self, width: usize, height: usize) -> Vec<(Vec<usize>, Region)> {
let mut stack: Vec<(Vec<usize>, &Layout, Region)> =
vec![(Vec::new(), self, Region::new(0, 0, width, height))];
let mut regions: Vec<(Vec<usize>, &Layout, Region)> = Vec::new();
while let Some(entry) = stack.pop() {
let (path, layout, region) = &entry;
let visible: Vec<(usize, &Layout)> = layout
.children
.iter()
.enumerate()
.filter(|(_, child)| child.visible)
.collect();
if !visible.is_empty() {
let children: Vec<&Layout> = visible.iter().map(|(_, child)| *child).collect();
for ((index, child), child_region) in visible
.iter()
.zip(layout.splitter.divide(&children, *region))
{
let mut child_path = path.clone();
child_path.push(*index);
stack.push((child_path, child, child_region));
}
}
regions.push(entry);
}
regions.sort_by_key(|(_, _, region)| *region);
regions
.into_iter()
.map(|(path, _, region)| (path, region))
.collect()
}
pub fn render_regions(&self, console: &Console, options: &ConsoleOptions) -> Vec<LayoutRender> {
let width = options.max_width;
let height = options
.height
.filter(|&height| height > 0)
.unwrap_or_else(|| console.height());
self.region_map(width, height)
.into_iter()
.filter_map(|(path, region)| {
let layout = self.at_path(&path)?;
if !layout.children().is_empty() {
return None;
}
let leaf_options = options.update_dimensions(region.width, region.height);
let render = match &layout.renderable {
Some(renderable) => {
console.render_lines(renderable.as_ref(), &leaf_options, true)
}
None => console.render_lines(&Placeholder { layout }, &leaf_options, true),
};
Some(LayoutRender {
name: layout.name.clone(),
path,
region,
render,
})
})
.collect()
}
fn path_of(&self, name: &str) -> Option<Vec<usize>> {
if self.name.as_deref() == Some(name) {
return Some(Vec::new());
}
self.children.iter().enumerate().find_map(|(index, child)| {
child.path_of(name).map(|mut path| {
path.insert(0, index);
path
})
})
}
pub fn refresh_screen(
&self,
console: &Console,
layout_name: &str,
) -> crate::errors::Result<bool> {
let Some(path) = self.path_of(layout_name) else {
return Ok(false);
};
let Some(layout) = self.at_path(&path) else {
return Ok(false);
};
let region = self
.render_map
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.iter()
.find(|entry| entry.path == path)
.map(|entry| entry.region);
let Some(Region {
x,
y,
width,
height,
}) = region
else {
return Ok(false);
};
let lines = console.render_lines(
layout,
&console.options().update_dimensions(width, height),
true,
);
if let Some(entry) = self
.render_map
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.iter_mut()
.find(|entry| entry.path == path)
{
entry.render = lines.clone();
}
console.update_screen_lines(&lines, x, y)?;
Ok(true)
}
pub fn map(&self) -> Vec<LayoutRender> {
self.render_map
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone()
}
}
impl std::ops::Index<&str> for Layout {
type Output = Layout;
fn index(&self, name: &str) -> &Layout {
self.get(name)
.unwrap_or_else(|| panic!("No layout with name {name:?}"))
}
}
impl std::ops::IndexMut<&str> for Layout {
fn index_mut(&mut self, name: &str) -> &mut Layout {
self.get_mut(name)
.unwrap_or_else(|| panic!("No layout with name {name:?}"))
}
}
fn py_repr_str(value: &str) -> String {
let quote = if value.contains('\'') && !value.contains('"') {
'"'
} else {
'\''
};
let mut out = String::with_capacity(value.len() + 2);
out.push(quote);
for ch in value.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
ch if ch == quote => {
out.push('\\');
out.push(ch);
}
ch if (ch as u32) < 0x20 || ch as u32 == 0x7f => {
out.push_str(&format!("\\x{:02x}", ch as u32));
}
ch => out.push(ch),
}
}
out.push(quote);
out
}
struct LayoutRepr {
fields: Vec<String>,
dim: bool,
}
impl LayoutRepr {
fn repr(&self, max_width: usize) -> String {
let one_line = format!("Layout({})", self.fields.join(", "));
if self.fields.is_empty() || cell_len(&one_line) <= max_width {
return one_line;
}
let last = self.fields.len() - 1;
let mut out = String::from("Layout(\n");
for (index, field) in self.fields.iter().enumerate() {
out.push_str(" ");
out.push_str(field);
if index != last {
out.push(',');
}
out.push('\n');
}
out.push(')');
out
}
}
impl Renderable for LayoutRepr {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let mut text = Text::new(self.repr(options.max_width));
ReprHighlighter::new().highlight(&mut text);
let segments = text.rich_render(console, options);
if self.dim {
Segment::apply_style(&segments, &Style::parse("dim").unwrap_or_default())
} else {
segments
}
}
fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
let width = self
.repr(options.max_width)
.lines()
.map(cell_len)
.max()
.unwrap_or(0);
Measurement::new(width, width)
}
}
struct Placeholder<'a> {
layout: &'a Layout,
}
impl Renderable for Placeholder<'_> {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let width = options.max_width;
let height = options
.height
.filter(|&height| height > 0)
.unwrap_or(options.size.height);
let title = match &self.layout.name {
Some(name) => format!("{} ({width} x {height})", py_repr_str(name)),
None => format!("({width} x {height})"),
};
let mut title = Text::new(title);
ReprHighlighter::new().highlight(&mut title);
let repr = LayoutRepr {
fields: self.layout.repr_fields(),
dim: false,
};
Panel::new(Box::new(
Align::center(Box::new(repr)).vertical(VerticalAlign::Middle),
))
.title_as_text(title)
.border_style("blue")
.height(height)
.rich_render(console, options)
}
}
impl Renderable for Layout {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let width = if options.max_width > 0 {
options.max_width
} else {
console.width()
};
let height = options
.height
.filter(|&height| height > 0)
.unwrap_or_else(|| console.height());
let render_map = self.render_regions(console, &options.update_dimensions(width, height));
let mut lines: Vec<Vec<Segment>> = vec![Vec::new(); height];
for leaf in &render_map {
let Region { y, height, .. } = leaf.region;
for (row, line) in lines.iter_mut().skip(y).take(height).zip(&leaf.render) {
row.extend(line.iter().cloned());
}
}
*self
.render_map
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = render_map;
let mut segments = Vec::new();
let last = lines.len().saturating_sub(1);
for (index, line) in lines.into_iter().enumerate() {
segments.extend(line);
if index != last {
segments.push(Segment::line());
}
}
segments
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::ColorSystem;
use crate::text::Text;
fn console(width: usize, height: usize) -> Console {
Console::builder()
.force_terminal(true)
.color_system(Some(ColorSystem::Truecolor))
.width(width)
.height(height)
.build()
}
fn leaf(s: &str) -> Layout {
Layout::with_renderable(Box::new(Text::new(s)))
}
fn cell(text: &str, width: usize) -> String {
format!("{text}{}", " ".repeat(width - text.chars().count()))
}
#[test]
fn column_split_stacks() {
let c = console(24, 4);
let mut lay = Layout::new();
lay.split_column(vec![leaf("top"), leaf("bottom")]);
let blank = " ".repeat(24);
let expected = format!(
"{}\n{blank}\n{}\n{blank}\n",
cell("top", 24),
cell("bottom", 24)
);
assert_eq!(c.capture(|con| con.print(&lay)), expected);
}
#[test]
fn row_split_side_by_side() {
let c = console(24, 4);
let mut lay = Layout::new();
lay.split_row(vec![leaf("L"), leaf("R")]);
let blank = " ".repeat(24);
let row0 = format!("{}{}", cell("L", 12), cell("R", 12));
let expected = format!("{row0}\n{blank}\n{blank}\n{blank}\n");
assert_eq!(c.capture(|con| con.print(&lay)), expected);
}
}