mod caret;
mod chrome;
mod selection_overlay;
mod single_row;
use rdom_core::{Dom, NodeId, NodeType};
use crate::ext::TuiExt;
use crate::layout::LayoutRect;
use crate::node::TuiNodeExt;
use crate::render::{Buffer, Rect};
use crate::style::ComputedStyle;
use super::text::{
advance_text_by_cells, glyph_style_from_computed, paint_text, paint_text_from, pseudo_style,
style_from_computed,
};
use chrome::inline_chrome;
use selection_overlay::apply_selection_overlay;
use single_row::paint_single_row_chrome;
pub(super) use caret::paint_caret_if_editable;
pub(crate) use chrome::{ChromeText, InlineChromeFn};
pub(super) fn paint_inline_content(
dom: &Dom<TuiExt>,
id: NodeId,
computed: &ComputedStyle,
inner: LayoutRect,
buf: &mut Buffer,
clip: Rect,
) {
if dom
.node(id)
.ext()
.is_some_and(|e| !e.anonymous_blocks.is_empty())
{
return;
}
let avail_single_row = inner.width;
if let Some((text, style)) = inline_chrome(dom, id, computed, avail_single_row) {
paint_single_row_chrome(dom, id, &text, style, inner, buf, clip);
return;
}
if let Some(layout) = dom
.node(id)
.tui_ext()
.and_then(|e| e.inline_layout.as_ref())
{
paint_lines(dom, id, layout, inner, buf, clip);
return;
}
if computed.flow == crate::layout::Flow::Block
&& dom
.node(id)
.child_nodes()
.any(|c| crate::render::layout_pass::is_in_flow(dom, c.id()))
{
return;
}
paint_single_row_chrome(
dom,
id,
&own_text_content(dom, id),
glyph_style_from_computed(computed),
inner,
buf,
clip,
);
}
fn paint_lines(
dom: &Dom<TuiExt>,
id: NodeId,
layout: &crate::render::inline::InlineLayout,
inner: LayoutRect,
buf: &mut Buffer,
clip: Rect,
) {
let first_visible_line = dom.node(id).ext().map_or(0, |e| e.scroll_y as i32);
paint_inline_layout(dom, layout, inner, first_visible_line, id, buf, clip);
if let Some(href) = anchor_href_for(dom, id) {
for (line_index, line) in layout.lines.iter().enumerate() {
let line_y = inner.y + line_index as i32;
if line_y < clip.y as i32 || line_y >= clip.bottom() as i32 {
continue;
}
let start_x = inner.x.max(clip.x as i32) as u16;
let cells = line.width;
if cells > 0 {
buf.set_link_range(start_x, line_y as u16, cells, Some(&href));
}
}
}
}
fn anchor_href_for(dom: &Dom<TuiExt>, id: NodeId) -> Option<String> {
let mut cur = Some(id);
while let Some(n) = cur {
let node = dom.node(n);
if node.tag_name() == Some("a")
&& let Some(href) = node.get_attribute("href")
{
return Some(href.to_string());
}
cur = node.parent_node().map(|p| p.id());
}
None
}
pub(super) fn paint_ifc(
dom: &Dom<TuiExt>,
id: NodeId,
_block_computed: &ComputedStyle,
inner: LayoutRect,
buf: &mut Buffer,
clip: Rect,
) {
let Some(inline_layout) = dom
.node(id)
.tui_ext()
.and_then(|e| e.inline_layout.as_ref())
else {
return;
};
let first_visible_line = dom.node(id).ext().map_or(0, |e| e.scroll_y as i32);
paint_inline_layout(dom, inline_layout, inner, first_visible_line, id, buf, clip);
}
pub(super) fn paint_anonymous_blocks(
dom: &Dom<TuiExt>,
container_id: NodeId,
buf: &mut Buffer,
clip: Rect,
) {
let Some(ext) = dom.node(container_id).tui_ext() else {
return;
};
for anon in &ext.anonymous_blocks {
paint_inline_layout(
dom,
&anon.inline_layout,
anon.rect,
0,
container_id,
buf,
clip,
);
}
}
fn paint_inline_layout(
dom: &Dom<TuiExt>,
inline_layout: &crate::render::inline::InlineLayout,
inner: LayoutRect,
first_visible_line: i32,
bg_dedup_owner: NodeId,
buf: &mut Buffer,
clip: Rect,
) {
let selection_range = dom.selection_range().filter(|r| !r.is_collapsed());
for (line_index, line) in inline_layout.lines.iter().enumerate() {
let line_y = inner.y + line_index as i32;
if line_y < clip.y as i32 || line_y >= clip.bottom() as i32 {
continue;
}
let li = line_index as i32;
if li < first_visible_line || li >= first_visible_line + inner.height as i32 {
continue;
}
let line_right = clip
.right()
.min(inner.x.saturating_add(inner.width as i32).max(0) as u16);
for generated in &line.generated {
paint_generated(
dom,
generated,
inner.x,
line_y as u16,
clip.x,
line_right,
buf,
);
}
for fragment in &line.fragments {
let frag_x = inner.x + fragment.x as i32;
if frag_x >= clip.right() as i32 {
continue;
}
if fragment.atomic {
let atom_rect = LayoutRect::new(frag_x, line_y, fragment.width, 1);
let atom_computed = dom
.node(fragment.node)
.ext()
.and_then(|e| e.computed.clone())
.unwrap_or_else(|| std::rc::Rc::new(ComputedStyle::initial()));
if atom_computed.position != crate::layout::Position::Static {
continue;
}
paint_inline_content(dom, fragment.node, &atom_computed, atom_rect, buf, clip);
continue;
}
let computed = dom
.node(fragment.node)
.ext()
.and_then(|e| e.computed.clone())
.unwrap_or_else(|| std::rc::Rc::new(ComputedStyle::initial()));
let style = if fragment.node == bg_dedup_owner {
glyph_style_from_computed(&computed)
} else {
style_from_computed(&computed)
};
let start_x = frag_x.max(clip.x as i32) as u16;
let skip = start_x as i32 - frag_x;
let budget_right = clip
.right()
.min(inner.x.saturating_add(inner.width as i32).max(0) as u16);
if start_x >= budget_right {
continue;
}
let max_width = budget_right - start_x;
let text_to_paint: &str = if skip > 0 {
advance_text_by_cells(&fragment.text, skip as u16)
} else {
&fragment.text
};
paint_text(
buf,
start_x,
line_y as u16,
budget_right,
text_to_paint,
style,
);
if let Some(href) = anchor_href_for(dom, fragment.node) {
let written_cells = text_to_paint
.chars()
.map(|_| 1u16)
.sum::<u16>()
.min(max_width);
if written_cells > 0 {
buf.set_link_range(start_x, line_y as u16, written_cells, Some(&href));
}
}
if let Some(ref sr) = selection_range {
apply_selection_overlay(dom, buf, line_y as u16, frag_x, clip, fragment, sr);
}
}
}
}
fn paint_generated(
dom: &Dom<TuiExt>,
generated: &crate::render::inline::GeneratedFragment,
origin_x: i32,
y: u16,
clip_left: u16,
right: u16,
buf: &mut Buffer,
) {
let node = dom.node(generated.host);
let computed = match generated.slot {
crate::ext::PseudoSlot::Before => node.computed_before(),
crate::ext::PseudoSlot::After => node.computed_after(),
};
let Some(computed) = computed else {
return;
};
let style = pseudo_style(
computed,
presentation_of(dom, generated.host, generated.slot.into()),
);
let x = origin_x + i32::from(generated.x);
let end = paint_text_from(buf, x, y, clip_left, right, &generated.text, style);
if let Some(href) = anchor_href_for(dom, generated.host) {
let start = x.max(i32::from(clip_left));
let end = end.min(i32::from(right));
if end > start {
buf.set_link_range(start as u16, y, (end - start) as u16, Some(&href));
}
}
}
fn presentation_of(
dom: &Dom<TuiExt>,
id: NodeId,
slot: crate::ext::StyleSlot,
) -> &crate::ext::PresentationStyle {
static EMPTY: std::sync::LazyLock<crate::ext::PresentationStyle> =
std::sync::LazyLock::new(crate::ext::PresentationStyle::default);
dom.node(id)
.ext()
.and_then(|e| e.presentation_for(slot))
.unwrap_or(&EMPTY)
}
fn own_text_content(dom: &Dom<TuiExt>, id: NodeId) -> String {
let mut out = String::new();
for child in dom.node(id).child_nodes() {
if child.node_type() == NodeType::Text
&& let Some(data) = child.node_value()
{
out.push_str(data);
}
}
out
}