mod border;
mod border_join;
mod inline_paint;
mod positioned_pseudos;
pub(crate) mod scrollbar;
mod text;
mod tree_guides;
#[cfg(test)]
mod tests;
use rdom_core::{Dom, NodeId, NodeType};
use crate::ext::TuiExt;
use crate::layout::{Display, LayoutRect, Overflow};
use crate::node::TuiNodeExt;
use crate::render::layout_pass::is_ifc_block;
use crate::render::{Buffer, Rect};
use crate::style::{Color, ComputedStyle};
use border::{fill_bg, paint_border};
use inline_paint::{
paint_anonymous_blocks, paint_caret_if_editable, paint_ifc, paint_inline_content,
};
pub trait PaintExt {
fn paint_dom(&self, buf: &mut Buffer, clip: Rect);
}
impl PaintExt for Dom<TuiExt> {
fn paint_dom(&self, buf: &mut Buffer, clip: Rect) {
paint_node(self, self.root(), buf, clip);
paint_z_list(self, buf, clip);
positioned_pseudos::paint_positioned_pseudos(self, buf, clip);
paint_modal_backdrops(self, buf, clip);
tree_guides::paint_tree_guides(self, buf, clip);
border_join::join_borders(self, buf);
}
}
fn paint_z_list(dom: &Dom<TuiExt>, buf: &mut Buffer, clip: Rect) {
let mut list: Vec<(i16, usize, NodeId)> = Vec::new();
let mut order: usize = 0;
collect_z_list(dom, dom.root(), &mut list, &mut order);
list.sort_by_key(|(z, ord, _)| (*z, *ord));
for (_, _, id) in list {
paint_node(dom, id, buf, clip);
}
}
fn collect_z_list(
dom: &Dom<TuiExt>,
id: NodeId,
out: &mut Vec<(i16, usize, NodeId)>,
order: &mut usize,
) {
if dom.node(id).node_type() == NodeType::Element {
let computed = dom.node(id).ext().and_then(|e| e.computed.as_ref());
let positioned = computed
.map(|c| {
matches!(
c.position,
crate::layout::Position::Absolute | crate::layout::Position::Fixed
)
})
.unwrap_or(false);
if positioned {
let z = computed
.map(|c| match c.z_index {
crate::layout::ZIndex::Auto => 0,
crate::layout::ZIndex::Value(n) => n,
})
.unwrap_or(0);
out.push((z, *order, id));
*order += 1;
}
}
for child in dom.node(id).child_nodes() {
match child.node_type() {
NodeType::Element | NodeType::Fragment => {
collect_z_list(dom, child.id(), out, order);
}
_ => {}
}
}
}
fn paint_modal_backdrops(dom: &Dom<TuiExt>, buf: &mut Buffer, clip: Rect) {
let mut modals: Vec<NodeId> = Vec::new();
collect_modal_dialogs(dom, dom.root(), &mut modals);
for dialog_id in modals {
let Some(backdrop_style) = dom
.node(dialog_id)
.ext()
.and_then(|e| e.computed_backdrop.clone())
else {
continue;
};
fill_backdrop(buf, clip, &backdrop_style);
paint_node(dom, dialog_id, buf, clip);
}
}
fn collect_modal_dialogs(dom: &Dom<TuiExt>, id: NodeId, out: &mut Vec<NodeId>) {
let node = dom.node(id);
if node.tag_name() == Some("dialog")
&& node.has_attribute("open")
&& node.has_attribute("data-rdom-modal")
{
out.push(id);
}
for child in node.child_nodes() {
collect_modal_dialogs(dom, child.id(), out);
}
}
fn fill_backdrop(buf: &mut Buffer, clip: Rect, style: &ComputedStyle) {
let bg = style.bg;
let fg = style.fg;
for y in clip.y..clip.bottom() {
for x in clip.x..clip.right() {
let Some(cell) = buf.cell_mut(x, y) else {
continue;
};
if bg != Color::Reset {
cell.set_bg(bg);
}
if fg != Color::Reset {
cell.set_fg(fg);
}
}
}
}
fn paint_node(dom: &Dom<TuiExt>, id: NodeId, buf: &mut Buffer, clip: Rect) {
let ty = dom.node(id).node_type();
if ty == NodeType::Fragment {
recurse_children(dom, id, buf, clip);
return;
}
if ty != NodeType::Element {
return;
}
let mut computed = dom
.node(id)
.computed()
.cloned()
.unwrap_or_else(ComputedStyle::initial);
if let Some(ext) = dom.node(id).ext() {
if let Some(fg) = ext.presentation.fg {
computed.fg = fg;
}
if let Some(bg) = ext.presentation.bg {
computed.bg = bg;
}
if let Some(border_fg) = ext.presentation.border_fg {
computed.border_fg = border_fg;
}
if let Some(padding) = &ext.presentation.padding {
computed.padding = padding.clone();
}
if let Some(gap) = ext.presentation.gap {
computed.gap = gap;
}
}
if computed.display == Display::None {
return;
}
let parent_bg = resolve_parent_bg(dom, id);
let saved_ctx = buf.enter_compose_ctx(computed.opacity, parent_bg);
let outer = dom.node(id).layout_rect().unwrap_or_default();
let inner = dom.node(id).content_layout_rect().unwrap_or(outer);
let padding_box = rdom_style::layout::compute_padding_box(outer, computed.border);
if let Some(outer_grid) = layout_rect_to_grid(outer, clip) {
let is_tree_row = dom.node(id).get_attribute("role") == Some("treeitem");
if computed.bg != Color::Reset && !is_tree_row {
let fill_area = if border_has_half_block(computed.border)
&& let Some(pb_grid) = layout_rect_to_grid(padding_box, clip)
{
pb_grid
} else {
outer_grid
};
fill_bg(buf, fill_area, computed.bg, computed.opacity);
}
if !computed.border.is_empty() {
let priority = compute_border_priority(dom, id);
paint_border(
buf,
outer,
computed.border,
computed.border_fg,
clip,
priority,
);
}
} else {
}
let clips = !matches!(computed.overflow_x, Overflow::Visible)
|| !matches!(computed.overflow_y, Overflow::Visible);
let children_clip = if clips {
layout_rect_to_grid(padding_box, clip).unwrap_or_else(|| Rect::new(clip.x, clip.y, 0, 0))
} else {
clip
};
if let Some(paint) = dom.node(id).ext().and_then(|e| e.canvas_paint.clone()) {
let mut ctx = crate::runtime::builtins::canvas::RenderContext::new(
buf,
inner.x,
inner.y,
inner.width,
inner.height,
children_clip,
);
paint.call(dom, &mut ctx);
scrollbar::paint_scrollbars(dom, id, &computed, buf, clip);
buf.exit_compose_ctx(saved_ctx);
return;
}
if is_ifc_block(dom, id) {
paint_ifc(dom, id, &computed, inner, buf, children_clip);
paint_caret_if_editable(dom, buf, id, children_clip);
scrollbar::paint_scrollbars(dom, id, &computed, buf, clip);
buf.exit_compose_ctx(saved_ctx);
return;
}
paint_inline_content(dom, id, &computed, inner, buf, children_clip);
if crate::render::inline::has_inline_layout(dom, id) {
paint_caret_if_editable(dom, buf, id, children_clip);
}
recurse_children(dom, id, buf, children_clip);
paint_anonymous_blocks(dom, id, buf, children_clip);
scrollbar::paint_scrollbars(dom, id, &computed, buf, clip);
buf.exit_compose_ctx(saved_ctx);
}
fn recurse_children(dom: &Dom<TuiExt>, id: NodeId, buf: &mut Buffer, clip: Rect) {
for child in dom.node(id).child_nodes() {
let cid = child.id();
match child.node_type() {
NodeType::Element => {
let computed_ref = child.ext().and_then(|e| e.computed.as_ref());
let is_inline = computed_ref
.map(|c| c.display == Display::Inline)
.unwrap_or(false);
let has_inline_layout =
child.ext().and_then(|e| e.inline_layout.as_ref()).is_some();
if is_inline && !has_inline_layout {
continue;
}
let is_positioned = computed_ref
.map(|c| {
matches!(
c.position,
crate::layout::Position::Absolute | crate::layout::Position::Fixed
)
})
.unwrap_or(false);
if is_positioned {
continue;
}
paint_node(dom, cid, buf, clip);
}
NodeType::Fragment => paint_node(dom, cid, buf, clip),
NodeType::Text | NodeType::Comment => {} }
}
}
pub(super) fn layout_rect_to_grid(layout: LayoutRect, clip: Rect) -> Option<Rect> {
let left = layout.x;
let top = layout.y;
let right = layout.x + layout.width as i32;
let bottom = layout.y + layout.height as i32;
let cl = clip.x as i32;
let ct = clip.y as i32;
let cr = clip.right() as i32;
let cb = clip.bottom() as i32;
let x = left.max(cl);
let y = top.max(ct);
let r = right.min(cr);
let b = bottom.min(cb);
if r <= x || b <= y {
return None;
}
Some(Rect::new(
x as u16,
y as u16,
(r - x) as u16,
(b - y) as u16,
))
}
fn compute_border_priority(dom: &Dom<TuiExt>, id: NodeId) -> u64 {
use crate::render::buffer::BorderContribution;
let mut depth: u16 = 0;
let mut cur = dom.node(id).parent_node();
while let Some(node) = cur {
depth = depth.saturating_add(1);
cur = node.parent_node();
}
BorderContribution::pack_priority(depth, id.as_u32())
}
fn border_has_half_block(border: rdom_style::layout::Border) -> bool {
use rdom_style::layout::BorderStyle;
matches!(border.top, BorderStyle::HalfBlock)
|| matches!(border.right, BorderStyle::HalfBlock)
|| matches!(border.bottom, BorderStyle::HalfBlock)
|| matches!(border.left, BorderStyle::HalfBlock)
}
fn resolve_parent_bg(dom: &Dom<TuiExt>, id: NodeId) -> Color {
let mut cur = dom.node(id).parent_node();
while let Some(node) = cur {
if let Some(c) = node.ext().and_then(|e| e.computed.as_ref())
&& c.bg != Color::Reset
{
return c.bg;
}
cur = node.parent_node();
}
Color::Reset
}