mod border;
mod border_join;
mod group;
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};
use crate::node::TuiNodeExt;
use crate::render::layout_pass::is_ifc_block;
use crate::render::stacking::{
LayerEntry, children_clip, collect_layers, creates_stacking_context, is_positioned,
};
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(crate) use inline_paint::{ChromeText, InlineChromeFn};
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_stacking_context(self, self.root(), buf, clip, 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_stacking_context(
dom: &Dom<TuiExt>,
root: NodeId,
buf: &mut Buffer,
clip: Rect,
viewport: Rect,
) {
let alpha = dom.node(root).computed().map_or(1.0, |c| c.opacity);
if alpha < 1.0 && dom.node(root).node_type() == NodeType::Element {
group::paint_group(dom, root, buf, alpha, |layer| {
paint_stacking_context_body(dom, root, layer, clip, viewport);
});
return;
}
paint_stacking_context_body(dom, root, buf, clip, viewport);
}
fn paint_stacking_context_body(
dom: &Dom<TuiExt>,
root: NodeId,
buf: &mut Buffer,
clip: Rect,
viewport: Rect,
) {
if dom.node(root).node_type() == NodeType::Fragment {
let layers = collect_layers(dom, root, clip, viewport);
paint_layers(dom, &layers.negative, buf, viewport);
recurse_children(dom, root, buf, clip, viewport);
paint_layers(dom, &layers.zero_auto, buf, viewport);
paint_layers(dom, &layers.positive, buf, viewport);
return;
}
let Some(frame) = paint_box(dom, root, buf, clip) else {
return;
};
let layers = collect_layers(dom, root, frame.children_clip, viewport);
paint_layers(dom, &layers.negative, buf, viewport);
paint_content(dom, root, buf, clip, viewport, &frame);
paint_layers(dom, &layers.zero_auto, buf, viewport);
paint_layers(dom, &layers.positive, buf, viewport);
}
fn paint_layers(dom: &Dom<TuiExt>, entries: &[LayerEntry], buf: &mut Buffer, viewport: Rect) {
for e in entries {
if e.context {
paint_stacking_context(dom, e.id, buf, e.clip, viewport);
} else {
paint_plain(dom, e.id, buf, e.clip, viewport);
}
}
}
fn paint_plain(dom: &Dom<TuiExt>, id: NodeId, buf: &mut Buffer, clip: Rect, viewport: Rect) {
let Some(frame) = paint_box(dom, id, buf, clip) else {
return;
};
paint_content(dom, id, buf, clip, viewport, &frame);
}
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_stacking_context(dom, dialog_id, buf, clip, 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);
}
}
}
}
struct BoxFrame {
computed: ComputedStyle,
inner: LayoutRect,
children_clip: Rect,
}
fn paint_box(dom: &Dom<TuiExt>, id: NodeId, buf: &mut Buffer, clip: Rect) -> Option<BoxFrame> {
if dom.node(id).node_type() != NodeType::Element {
return None;
}
let mut computed = dom
.node(id)
.computed()
.cloned()
.unwrap_or_else(ComputedStyle::initial);
if let Some(presentation) = dom.node(id).ext().and_then(|e| e.presentation.as_deref()) {
if let Some(fg) = presentation.fg {
computed.fg = fg;
}
if let Some(bg) = presentation.bg {
computed.bg = bg;
}
if let Some(border_fg) = presentation.border_fg {
computed.border_fg = border_fg;
}
if let Some(padding) = &presentation.padding {
computed.padding = padding.clone();
}
if let Some(gap) = presentation.gap {
computed.gap = crate::layout::GapValue::Cells(gap);
}
}
if computed.display == Display::None {
return None;
}
let outer = dom.node(id).layout_rect().unwrap_or_default();
let inner = dom.node(id).content_layout_rect().unwrap_or(outer);
let padding_box = crate::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);
}
if !computed.border.is_empty() {
let priority = compute_border_priority(dom, id);
paint_border(
buf,
outer,
computed.border,
computed.border_fg,
clip,
priority,
);
}
}
let children_clip = children_clip(dom, id, &computed, clip);
Some(BoxFrame {
computed,
inner,
children_clip,
})
}
fn paint_content(
dom: &Dom<TuiExt>,
id: NodeId,
buf: &mut Buffer,
clip: Rect,
viewport: Rect,
frame: &BoxFrame,
) {
let computed = &frame.computed;
let inner = frame.inner;
let children_clip = frame.children_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);
return;
}
if is_ifc_block(dom, id) {
let text_inner = crate::render::inline::scrolled_content_rect(dom, id).unwrap_or(inner);
paint_ifc(dom, id, computed, text_inner, buf, children_clip);
paint_caret_if_editable(dom, buf, id, children_clip);
scrollbar::paint_scrollbars(dom, id, computed, buf, clip);
return;
}
let text_inner = crate::render::inline::scrolled_content_rect(dom, id).unwrap_or(inner);
paint_inline_content(dom, id, computed, text_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, viewport);
paint_anonymous_blocks(dom, id, buf, children_clip);
scrollbar::paint_scrollbars(dom, id, computed, buf, clip);
}
fn recurse_children(dom: &Dom<TuiExt>, id: NodeId, buf: &mut Buffer, clip: Rect, viewport: 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;
}
match computed_ref {
Some(c) if is_positioned(c) => continue,
Some(c) if creates_stacking_context(c) => {
paint_stacking_context(dom, cid, buf, clip, viewport);
}
_ => paint_plain(dom, cid, buf, clip, viewport),
}
}
NodeType::Fragment => recurse_children(dom, cid, buf, clip, viewport),
_ => {}
}
}
}
pub(crate) 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)
}