mod caret;
pub(crate) mod generated;
mod packer;
#[cfg(test)]
mod tests;
use rdom_core::{Dom, NodeId, NodeType};
use crate::ext::{PseudoSlot, StyleSlot, TuiExt};
use crate::layout::WhiteSpace;
pub use caret::cell_of_position;
pub(crate) use caret::cells_before_byte;
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, PartialEq, Eq)]
pub struct GeneratedFragment {
pub host: NodeId,
pub slot: PseudoSlot,
pub x: u16,
pub width: u16,
pub text: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct LineBox {
pub fragments: Vec<InlineFragment>,
pub generated: Vec<GeneratedFragment>,
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 child = text_node;
let mut cur = dom.node(text_node).parent_node().map(|p| p.id());
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()
&& !ext.anonymous_blocks.is_empty()
&& let Some(index) = dom.node(id).child_nodes().position(|c| c.id() == child)
&& let Some(i) = ext
.anonymous_blocks
.iter()
.position(|anon| anon.child_range.0 <= index && index < anon.child_range.1)
{
return Some(InlineFlow::Anonymous {
container: id,
index: i,
});
}
child = id;
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)> {
match flow {
InlineFlow::Ifc { block } => {
let layout = dom.node(block).ext()?.inline_layout.as_ref()?;
let content = scrolled_content_rect(dom, block)?;
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 scrolled_content_rect(
dom: &Dom<TuiExt>,
block: NodeId,
) -> Option<crate::layout::LayoutRect> {
use crate::node::TuiNodeExt;
let mut content = dom.node(block).content_layout_rect()?;
let ext = dom.node(block).ext()?;
content.x -= ext.scroll_x as i32;
content.y -= ext.scroll_y as i32;
Some(content)
}
pub fn atomic_placements(
layout: &InlineLayout,
origin: crate::layout::LayoutRect,
) -> Vec<(NodeId, crate::layout::LayoutRect)> {
let mut atoms = Vec::new();
for (line_idx, line) in layout.lines.iter().enumerate() {
let line_y = origin.y + line_idx as i32;
for fragment in line.fragments.iter().filter(|f| f.atomic) {
atoms.push((
fragment.node,
crate::layout::LayoutRect::new(
origin.x + fragment.x as i32,
line_y,
fragment.width,
1,
),
));
}
}
atoms
}
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);
push_pseudo(dom, block, PseudoSlot::Before, &mut packer);
walk_subtree(dom, block, &mut packer);
push_pseudo(dom, block, PseudoSlot::After, &mut packer);
packer.finish();
InlineLayout {
lines: packer.take_lines(),
content_width,
}
}
fn push_pseudo<'a>(
dom: &'a Dom<TuiExt>,
host: NodeId,
slot: PseudoSlot,
packer: &mut LinePacker<'a>,
) {
if slot == PseudoSlot::Before {
for item in generated::deferred_markers(dom, host) {
if let Some(text) = generated::static_pseudo_text(dom, item, StyleSlot::Before) {
packer.push_generated(item, PseudoSlot::Before, text);
}
}
}
if let Some(text) = generated::own_inline_pseudo_text(dom, host, slot.into()) {
packer.push_generated(host, slot, text);
}
}
pub fn compute_inline_layout_for_run(
dom: &Dom<TuiExt>,
parent: NodeId,
direct_children: &[NodeId],
content_width: u16,
) -> InlineLayout {
let pseudos = generated::run_pseudos(dom, parent, direct_children);
pack_run(dom, parent, direct_children, pseudos, content_width)
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct RunPseudos {
pub(crate) before: bool,
pub(crate) after: bool,
}
pub(crate) fn pack_run(
dom: &Dom<TuiExt>,
parent: NodeId,
direct_children: &[NodeId],
pseudos: RunPseudos,
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);
if pseudos.before {
push_pseudo(dom, parent, PseudoSlot::Before, &mut packer);
}
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.content_width());
packer.push_atomic_inline_block(child_id, intrinsic);
continue;
}
walk_inline_box(dom, child_id, &mut packer);
}
_ => {}
}
}
if pseudos.after {
push_pseudo(dom, parent, PseudoSlot::After, &mut packer);
}
packer.finish();
InlineLayout {
lines: packer.take_lines(),
content_width,
}
}
fn walk_subtree<'a>(dom: &'a Dom<TuiExt>, id: NodeId, packer: &mut LinePacker<'a>) {
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.content_width(),
);
packer.push_atomic_inline_block(child.id(), intrinsic);
continue;
}
walk_inline_box(dom, child.id(), packer);
}
_ => {}
}
}
}
fn walk_inline_box<'a>(dom: &'a Dom<TuiExt>, id: NodeId, packer: &mut LinePacker<'a>) {
if let Some(text) = generated::static_pseudo_text(dom, id, StyleSlot::Before) {
packer.push_generated(id, PseudoSlot::Before, text);
}
walk_subtree(dom, id, packer);
if let Some(text) = generated::static_pseudo_text(dom, id, StyleSlot::After) {
packer.push_generated(id, PseudoSlot::After, text);
}
}
fn atomic_inline_block_intrinsic_width(
dom: &Dom<TuiExt>,
id: NodeId,
containing_block_width: u16,
) -> u16 {
crate::render::layout_pass::intrinsic::intrinsic_size(
dom,
id,
crate::layout::Direction::Row,
0,
containing_block_width,
)
}