mod packer;
#[cfg(test)]
mod tests;
use rdom_core::{Dom, NodeId, NodeType};
use crate::ext::TuiExt;
use crate::layout::WhiteSpace;
use packer::LinePacker;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InlineFragment {
pub node: NodeId,
pub text_node: NodeId,
pub source_byte_offset: usize,
pub x: u16,
pub width: u16,
pub text: String,
pub atomic: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct LineBox {
pub fragments: Vec<InlineFragment>,
pub width: u16,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InlineLayout {
pub lines: Vec<LineBox>,
pub content_width: u16,
}
impl InlineLayout {
pub fn height(&self) -> u16 {
self.lines.len() as u16
}
}
pub fn has_inline_layout(dom: &Dom<TuiExt>, id: NodeId) -> bool {
dom.node(id)
.ext()
.and_then(|e| e.inline_layout.as_ref())
.is_some()
}
pub fn inline_flow_container(dom: &Dom<TuiExt>, node_id: NodeId) -> Option<NodeId> {
let mut cur = Some(node_id);
while let Some(id) = cur {
if has_inline_layout(dom, id) {
return Some(id);
}
cur = dom.node(id).parent_node().map(|p| p.id());
}
None
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InlineFlow {
Ifc { block: NodeId },
Anonymous { container: NodeId, index: usize },
}
impl InlineFlow {
pub fn owner(&self) -> NodeId {
match self {
Self::Ifc { block } => *block,
Self::Anonymous { container, .. } => *container,
}
}
}
pub fn inline_flow_for_text(dom: &Dom<TuiExt>, text_node: NodeId) -> Option<InlineFlow> {
let mut cur = Some(text_node);
while let Some(id) = cur {
if has_inline_layout(dom, id) {
return Some(InlineFlow::Ifc { block: id });
}
if let Some(ext) = dom.node(id).ext() {
for (i, anon) in ext.anonymous_blocks.iter().enumerate() {
for line in &anon.inline_layout.lines {
for frag in &line.fragments {
if frag.text_node == text_node {
return Some(InlineFlow::Anonymous {
container: id,
index: i,
});
}
}
}
}
}
cur = dom.node(id).parent_node().map(|p| p.id());
}
None
}
pub fn inline_flow_layout(
dom: &Dom<TuiExt>,
flow: InlineFlow,
) -> Option<(&InlineLayout, crate::layout::LayoutRect)> {
use crate::node::TuiNodeExt;
match flow {
InlineFlow::Ifc { block } => {
let layout = dom.node(block).ext()?.inline_layout.as_ref()?;
let content = dom.node(block).content_layout_rect()?;
Some((layout, content))
}
InlineFlow::Anonymous { container, index } => {
let anon = dom.node(container).ext()?.anonymous_blocks.get(index)?;
Some((&anon.inline_layout, anon.rect))
}
}
}
pub fn compute_inline_layout(dom: &Dom<TuiExt>, block: NodeId, content_width: u16) -> InlineLayout {
let ws = dom
.node(block)
.ext()
.and_then(|e| e.computed.as_ref())
.map(|c| c.white_space)
.unwrap_or(WhiteSpace::Normal);
let mut packer = LinePacker::new(content_width, ws);
walk_subtree(dom, block, &mut packer);
packer.finish();
InlineLayout {
lines: packer.take_lines(),
content_width,
}
}
pub fn compute_inline_layout_for_run(
dom: &Dom<TuiExt>,
parent: NodeId,
direct_children: &[NodeId],
content_width: u16,
) -> InlineLayout {
let ws = dom
.node(parent)
.ext()
.and_then(|e| e.computed.as_ref())
.map(|c| c.white_space)
.unwrap_or(WhiteSpace::Normal);
use crate::layout::Display;
let mut packer = LinePacker::new(content_width, ws);
for &child_id in direct_children {
let child = dom.node(child_id);
match child.node_type() {
NodeType::Text => {
if let Some(data) = child.node_value() {
packer.push_text(parent, child_id, data);
}
}
NodeType::Element => {
if child.tag_name() == Some("br") {
packer.push_hard_break(child_id);
continue;
}
let display = child
.ext()
.and_then(|e| e.computed.as_ref())
.map(|c| c.display)
.unwrap_or(Display::Block);
if matches!(display, Display::InlineBlock) {
let intrinsic = atomic_inline_block_intrinsic_width(dom, child_id);
packer.push_atomic_inline_block(child_id, intrinsic);
continue;
}
walk_subtree(dom, child_id, &mut packer);
}
_ => {}
}
}
packer.finish();
InlineLayout {
lines: packer.take_lines(),
content_width,
}
}
fn walk_subtree(dom: &Dom<TuiExt>, id: NodeId, packer: &mut LinePacker) {
use crate::layout::Display;
for child in dom.node(id).child_nodes() {
match child.node_type() {
NodeType::Text => {
if let Some(data) = child.node_value() {
packer.push_text(id, child.id(), data);
}
}
NodeType::Element => {
use crate::layout::Position;
let (display, position) = child
.ext()
.and_then(|e| e.computed.as_ref())
.map(|c| (c.display, c.position))
.unwrap_or((Display::Block, Position::Static));
if display == Display::None
|| matches!(position, Position::Absolute | Position::Fixed)
{
continue;
}
if child.tag_name() == Some("br") {
packer.push_hard_break(child.id());
continue;
}
if matches!(display, Display::InlineBlock) {
let intrinsic = atomic_inline_block_intrinsic_width(dom, child.id());
packer.push_atomic_inline_block(child.id(), intrinsic);
continue;
}
walk_subtree(dom, child.id(), packer);
}
_ => {}
}
}
}
fn atomic_inline_block_intrinsic_width(dom: &Dom<TuiExt>, id: NodeId) -> u16 {
crate::render::layout_pass::intrinsic::intrinsic_size(dom, id, crate::layout::Direction::Row, 0)
}