use oino_ui::node::Node;
use ratatui::layout::Rect;
use super::Ctx;
use super::style::{BlockSpec, block};
use crate::theme::Theme;
#[must_use]
pub(crate) fn common_block(node: &Node) -> Option<BlockSpec<'_>> {
node.common().block.map(BlockSpec::from)
}
#[must_use]
pub(crate) fn inner(area: Rect, spec: Option<BlockSpec<'_>>, theme: &Theme) -> Rect {
match spec {
None => area,
Some(spec) => block(&spec, None, ratatui::style::Style::default(), theme).inner(area),
}
}
#[must_use]
pub(crate) fn block_chrome(spec: &BlockSpec<'_>) -> u16 {
let mut height = 0;
if let Some(borders) = spec.borders {
let sides: &[oino_ui::widgets::block::BorderSide] = match borders {
oino_ui::widgets::block::Borders::All => &[
oino_ui::widgets::block::BorderSide::TOP,
oino_ui::widgets::block::BorderSide::BOTTOM,
],
oino_ui::widgets::block::Borders::Sides(sides) => sides,
_ => &[],
};
for side in sides {
if matches!(
side,
oino_ui::widgets::block::BorderSide::TOP
| oino_ui::widgets::block::BorderSide::BOTTOM
) {
height += 1;
}
}
}
if let Some(padding) = spec.padding {
height += match padding {
oino_ui::widgets::block::Padding::Uniform(n) => n.saturating_mul(2),
oino_ui::widgets::block::Padding::Rect { top, bottom, .. } => {
top.unwrap_or(0) + bottom.unwrap_or(0)
}
};
}
height
}
#[allow(
clippy::too_many_lines,
reason = "one arm per node kind (ui-protocol §4.4)"
)]
#[must_use]
pub(crate) fn height(node: &Node, width: u16, cx: &Ctx<'_>) -> u16 {
if width == 0 {
return 0;
}
let spec = common_block(node);
let chrome = spec.map_or(0, |spec| block_chrome(&spec));
let inner_width = inner(
Rect {
x: 0,
y: 0,
width,
height: 1,
},
spec,
cx.theme,
)
.width;
let content = match node {
Node::Layout(layout) => layout_height(layout, inner_width, cx),
Node::Block(block) => {
let spec = BlockSpec::from(block);
let chrome = block_chrome(&spec);
let collapsed = block.collapsed.unwrap_or(false);
if collapsed {
chrome
} else if let Some(child) = &block.child {
let area = Rect {
x: 0,
y: 0,
width,
height: 1,
};
let inner =
super::style::block(&spec, None, ratatui::style::Style::default(), cx.theme)
.inner(area);
height(child, inner.width, cx).saturating_add(chrome)
} else {
chrome
}
}
Node::Paragraph(paragraph) => paragraph_height(paragraph, inner_width, cx)
.max(1)
.saturating_add(chrome),
Node::Tree(tree) => u16::try_from(super::focus::tree_rows(tree, "", cx.local).len())
.unwrap_or(u16::MAX)
.max(1)
.saturating_add(chrome),
Node::List(list) => {
let rows: u16 = list
.items
.iter()
.map(|item| match item {
oino_ui::widgets::list::ListItem::Text(text) => {
u16::try_from(text.lines().len()).unwrap_or(u16::MAX)
}
oino_ui::widgets::list::ListItem::Full {
content, detail, ..
} => u16::try_from(content.lines().len())
.unwrap_or(u16::MAX)
.saturating_add(u16::from(detail.is_some())),
})
.sum();
rows.max(1).saturating_add(chrome)
}
Node::Table(table) => {
let rows = |row: &oino_ui::widgets::table::Row| {
row.height.unwrap_or(1) + row.bottom_margin.unwrap_or(0)
};
let mut n = table.rows.iter().map(rows).sum::<u16>();
if let Some(header) = &table.header {
n += rows(header);
}
if let Some(footer) = &table.footer {
n += rows(footer);
}
n.max(1).saturating_add(chrome)
}
Node::Tabs(tabs) => {
let selected = super::extras::selected_tab(tabs);
let pane = selected
.and_then(|key| tabs.panes.as_ref().and_then(|panes| panes.get(key)))
.map_or(0, |pane| height(pane, inner_width, cx));
1_u16.saturating_add(pane).saturating_add(chrome)
}
Node::Gauge(_)
| Node::LineGauge(_)
| Node::KeyHints(_)
| Node::Spinner(_)
| Node::Button(_)
| Node::Toggle(_)
| Node::Select(_) => 1_u16.saturating_add(chrome),
Node::Scrollbar(scrollbar) => {
let height: u16 = match scrollbar.orientation.unwrap_or_default() {
oino_ui::widgets::scrollbar::ScrollbarOrientation::HorizontalTop
| oino_ui::widgets::scrollbar::ScrollbarOrientation::HorizontalBottom => 1,
_ => 3,
};
height.saturating_add(chrome)
}
Node::Sparkline(_) | Node::Image(_) | Node::Empty(_) => 3_u16.saturating_add(chrome),
Node::BarChart(_) => 8_u16.saturating_add(chrome),
Node::Chart(_) => 12_u16.saturating_add(chrome),
Node::Log(log) => {
let cap = log.max_lines.unwrap_or(2_000).min(5_000);
u16::try_from(log.lines.len())
.unwrap_or(u16::MAX)
.min(u16::try_from(cap).unwrap_or(u16::MAX))
.max(1)
.saturating_add(chrome)
}
Node::Markdown(markdown) => {
u16::try_from(super::extras::markdown_lines(markdown, inner_width, cx).len())
.unwrap_or(u16::MAX)
.max(1)
.saturating_add(chrome)
}
Node::Code(code) => u16::try_from(super::extras::code_lines(code, cx).len())
.unwrap_or(u16::MAX)
.max(1)
.saturating_add(chrome),
Node::Diff(diff) => u16::try_from(super::extras::diff_lines(diff, cx).len())
.unwrap_or(u16::MAX)
.max(1)
.saturating_add(chrome),
Node::Input(input) => {
let rows = if input.multiline.unwrap_or(false) {
input.rows.unwrap_or(3).max(1)
} else {
1
};
rows.saturating_add(chrome)
}
Node::Unknown(unknown) => unknown
.fallback
.as_deref()
.map_or(0, |fallback| height(fallback, width, cx)),
};
content.max(1)
}
fn layout_height(layout: &oino_ui::layout::Layout, width: u16, cx: &Ctx<'_>) -> u16 {
let collapsed = super::layout::collapses(layout, width);
let horizontal = matches!(
layout.direction,
Some(oino_ui::layout::Direction::Horizontal)
) && !collapsed;
let (horizontal_margin, vertical_margin) = match layout.margin {
None => (0, 0),
Some(oino_ui::layout::Margin::Uniform(n)) => (n, n),
Some(oino_ui::layout::Margin::Axes {
horizontal,
vertical,
}) => (horizontal, vertical),
};
let content_width = width.saturating_sub(horizontal_margin.saturating_mul(2));
let heights: Vec<u16> = layout
.children
.iter()
.map(|child| height(child, content_width, cx))
.collect();
let spacing = layout.spacing.unwrap_or(0);
if horizontal {
let tallest = heights.iter().copied().max().unwrap_or(1);
tallest.saturating_add(vertical_margin.saturating_mul(2))
} else {
let sum: u16 = heights.iter().copied().sum();
let gaps =
spacing.saturating_mul(u16::try_from(heights.len().saturating_sub(1)).unwrap_or(0));
sum.saturating_add(gaps)
.saturating_add(vertical_margin.saturating_mul(2))
.max(1)
}
}
fn paragraph_height(
paragraph: &oino_ui::widgets::paragraph::Paragraph,
width: u16,
cx: &Ctx<'_>,
) -> u16 {
use crate::render::text::{Run, wrap};
if matches!(
paragraph.wrap,
Some(oino_ui::widgets::paragraph::Wrap::Disabled(false))
) {
return u16::try_from(paragraph.text.lines().len()).unwrap_or(u16::MAX);
}
let base = cx.base();
let mut total = 0_usize;
for line in paragraph.text.lines() {
let runs: Vec<Run> = line
.spans()
.iter()
.map(|span| {
let style = super::style::patch(span.style(), base, cx.theme);
Run::new(span.content().to_owned(), style)
})
.collect();
total += wrap(&runs, usize::from(width.max(1)), &[], &[]).len();
}
u16::try_from(total).unwrap_or(u16::MAX)
}