mod block;
mod border;
mod button;
mod image;
mod input;
mod select;
mod text;
mod textarea;
use std::collections::HashMap;
use crossterm::style::Color;
use crate::{
graphics::{Block, FontStyle, ImageCache, InputRenderState, TermLayout},
html::css_parse::{Background, Position, Visibility},
layout::{BoxKind, LayoutBox, Rect, ReplacedContent},
tdom::node::{Node, NodeArena, NodeId, NodeType},
};
use self::block::paint_block;
use self::border::paint_borders;
use self::button::paint_button;
use self::image::paint_image;
use self::input::paint_input;
use self::select::paint_select;
use self::textarea::paint_textarea;
pub(super) const CONTROL_DEFAULT_BG: Color = Color::DarkGrey;
pub(super) const SELECTION_BG: Color = Color::Rgb {
r: 0x3b,
g: 0x42,
b: 0x61,
};
pub(super) fn control_clip(lb: &LayoutBox, canvas: &PaintCanvas<'_>, clip: Rect) -> Option<Rect> {
let r = clamp_rect_to_canvas(lb.rect, canvas.columns as usize)?;
intersect_rects(r, clip)
}
pub(super) fn caret_cell(
node: &Node,
fg: Option<Color>,
bg: Option<Color>,
) -> (Option<Color>, Option<Color>) {
match node.css_attr.get_caret_color() {
Some(cc) => (Some(bg.unwrap_or(Color::Black)), Some(cc)),
None => (
Some(bg.unwrap_or(Color::Black)),
Some(fg.unwrap_or(Color::White)),
),
}
}
pub(super) fn paint_control_chrome(
lb: &LayoutBox,
arena: &NodeArena,
canvas: &mut PaintCanvas<'_>,
rect: Rect,
fg: Option<Color>,
) -> (Rect, Background) {
let node = &arena[lb.node_id];
let bg = control_background(node);
let bg_fill = BackgroundFill {
background: &bg,
rect,
};
let cols = canvas.columns as usize;
let start = canvas.cell_index(rect.x, rect.y);
for row_off in 0..rect.height {
for col_off in 0..rect.width {
let idx = start.saturating_add(row_off.saturating_mul(cols).saturating_add(col_off));
if idx < canvas.layout.len() && canvas.in_region(idx) {
let cell = &mut canvas.layout[idx];
if cell.word.is_empty() {
cell.word.push(' ');
}
cell.bcolor = bg_fill.color_at(rect.x + col_off, rect.y + row_off);
}
}
}
let sides = &lb.paint_style.box_edges.border;
if sides.any_visible() {
paint_borders(lb, rect, canvas, fg, Some(bg_fill), rect);
}
(
Rect::new(
rect.x.saturating_add(sides.left.thickness()),
rect.y.saturating_add(sides.top.thickness()),
rect.width
.saturating_sub(sides.left.thickness())
.saturating_sub(sides.right.thickness()),
rect.height
.saturating_sub(sides.top.thickness())
.saturating_sub(sides.bottom.thickness()),
),
bg,
)
}
pub(super) fn control_background(node: &Node) -> Background {
node.css_attr
.get_background()
.unwrap_or(Background::Color(CONTROL_DEFAULT_BG))
}
pub(super) fn control_background_at(
background: &Background,
x: usize,
y: usize,
rect: Rect,
) -> Option<Color> {
background_at(Some(BackgroundFill { background, rect }), x, y)
}
pub(crate) use self::input::{range_is_vertical, range_spec, value_window};
pub(crate) use self::textarea::{
textarea_caret_position, textarea_char_at, textarea_scroll_row, textarea_visual_lines,
};
pub(super) struct PaintCanvas<'a> {
pub layout: &'a mut [Block],
pub columns: u16,
pub image_cache: &'a mut ImageCache,
pub(super) region: Option<Rect>,
pub input_states: &'a HashMap<NodeId, InputRenderState>,
pub open_select: Option<NodeId>,
pub scrollbars_visible: &'a HashMap<NodeId, bool>,
}
impl<'a> PaintCanvas<'a> {
pub(super) fn cell_index(&self, x: usize, y: usize) -> usize {
y.saturating_mul(self.columns as usize).saturating_add(x)
}
#[inline]
pub(super) fn in_region(&self, idx: usize) -> bool {
match self.region {
None => true,
Some(r) => {
let cols = self.columns as usize;
let y = idx / cols;
let x = idx - y * cols;
x >= r.x
&& x < r.x.saturating_add(r.width)
&& y >= r.y
&& y < r.y.saturating_add(r.height)
}
}
}
}
pub(super) fn cell_index(canvas: &PaintCanvas<'_>, x: usize, y: usize) -> usize {
canvas.cell_index(x, y)
}
pub(super) fn put(
canvas: &mut PaintCanvas<'_>,
idx: usize,
word: Option<char>,
fg: Option<Color>,
bg: Option<Color>,
) {
if idx >= canvas.layout.len() || !canvas.in_region(idx) {
return;
}
let cell = &mut canvas.layout[idx];
cell.word.clear();
if let Some(c) = word {
cell.word.push(c);
}
cell.fcolor = fg;
cell.bcolor = bg;
cell.style = FontStyle::empty();
}
pub fn paint_layout_tree(boxes: &[LayoutBox], arena: &NodeArena, term_layout: &mut TermLayout) {
let mut max_y = 0usize;
for b in boxes {
let bottom = b.rect.y.saturating_add(b.rect.height.max(1));
if bottom > max_y {
max_y = bottom;
}
}
if max_y as u32 > term_layout.rendered_rows {
term_layout.rendered_rows = max_y as u32;
}
let columns = term_layout.columns;
let mut canvas = PaintCanvas {
layout: &mut term_layout.layout,
columns,
image_cache: &mut term_layout.image_cache,
region: None,
input_states: &term_layout.input_states,
open_select: term_layout.open_select,
scrollbars_visible: &term_layout.scrollbars_visible,
};
for b in boxes {
paint_box(b, arena, &mut canvas, None, None);
}
fill_canvas_background(&mut canvas, boxes, arena, max_y);
}
pub fn paint_layout_tree_region(
boxes: &[LayoutBox],
arena: &NodeArena,
term_layout: &mut TermLayout,
region: Rect,
) {
let mut max_y = 0usize;
for b in boxes {
let bottom = b.rect.y.saturating_add(b.rect.height.max(1));
if bottom > max_y {
max_y = bottom;
}
}
if max_y as u32 > term_layout.rendered_rows {
term_layout.rendered_rows = max_y as u32;
}
let columns = term_layout.columns;
let mut canvas = PaintCanvas {
layout: &mut term_layout.layout,
columns,
image_cache: &mut term_layout.image_cache,
region: Some(region),
input_states: &term_layout.input_states,
open_select: term_layout.open_select,
scrollbars_visible: &term_layout.scrollbars_visible,
};
for b in boxes {
if intersect_rects(b.rect, region).is_some() {
paint_box(b, arena, &mut canvas, None, None);
}
}
fill_canvas_background(&mut canvas, boxes, arena, max_y);
}
fn fill_canvas_background(
canvas: &mut PaintCanvas<'_>,
boxes: &[LayoutBox],
arena: &NodeArena,
content_bottom: usize,
) {
let Some(bg) = find_canvas_background(boxes, arena) else {
return;
};
let cols = canvas.columns as usize;
let rows = canvas.layout.len() / cols;
let bg_rect = Rect::new(0, 0, cols, rows);
let start_row = content_bottom.min(rows);
for y in start_row..rows {
let row_start = y * cols;
for i in 0..cols {
let idx = row_start + i;
if !canvas.in_region(idx) {
continue;
}
let cell = &mut canvas.layout[idx];
if cell.word == " " && cell.bcolor.is_none() && cell.style.is_empty() {
cell.bcolor = bg.color_at(i, y, bg_rect.width, bg_rect.height);
}
}
}
}
fn find_canvas_background(boxes: &[LayoutBox], arena: &NodeArena) -> Option<Background> {
for b in boxes {
if let Some(bg) = box_canvas_background(b, arena) {
return Some(bg);
}
}
None
}
fn box_canvas_background(lb: &LayoutBox, arena: &NodeArena) -> Option<Background> {
let node = &arena[lb.node_id];
if node.node_type == NodeType::Html {
if !node.css_attr.background_clips_to_text()
&& let Some(bg) = node.css_attr.get_background()
{
return Some(bg);
}
for child in &lb.children {
if arena[child.node_id].node_type == NodeType::Body
&& !arena[child.node_id].css_attr.background_clips_to_text()
&& let Some(bg) = arena[child.node_id].css_attr.get_background()
{
return Some(bg);
}
}
}
for child in &lb.children {
if let Some(bg) = box_canvas_background(child, arena) {
return Some(bg);
}
}
None
}
#[derive(Clone, Copy)]
pub(super) struct BackgroundFill<'a> {
pub background: &'a Background,
pub rect: Rect,
}
impl BackgroundFill<'_> {
pub(super) fn color_at(&self, x: usize, y: usize) -> Option<Color> {
self.background.color_at(
x.saturating_sub(self.rect.x),
y.saturating_sub(self.rect.y),
self.rect.width,
self.rect.height,
)
}
}
pub(super) fn background_at(fill: Option<BackgroundFill<'_>>, x: usize, y: usize) -> Option<Color> {
fill.and_then(|fill| fill.color_at(x, y))
}
pub(super) fn text_color_at(
fg: Option<Color>,
fill: Option<BackgroundFill<'_>>,
x: usize,
y: usize,
) -> Option<Color> {
fg.or_else(|| background_at(fill, x, y))
}
pub(super) fn intersect_rects(a: Rect, b: Rect) -> Option<Rect> {
let x0 = a.x.max(b.x);
let y0 = a.y.max(b.y);
let x1 = a.x.saturating_add(a.width).min(b.x.saturating_add(b.width));
let y1 =
a.y.saturating_add(a.height)
.min(b.y.saturating_add(b.height));
if x1 <= x0 || y1 <= y0 {
None
} else {
Some(Rect::new(x0, y0, x1 - x0, y1 - y0))
}
}
pub(super) fn clamp_rect_to_canvas(r: Rect, canvas_cols: usize) -> Option<Rect> {
if r.x >= canvas_cols || r.width == 0 || r.height == 0 {
return None;
}
Some(Rect::new(
r.x,
r.y,
r.width.min(canvas_cols - r.x),
r.height,
))
}
fn paint_box(
lb: &LayoutBox,
arena: &NodeArena,
canvas: &mut PaintCanvas<'_>,
parent_bg: Option<BackgroundFill<'_>>,
clip: Option<Rect>,
) {
let canvas_cols = canvas.columns as usize;
let overflow_clips =
arena[lb.node_id].css_attr.get_overflow() == crate::html::css_parse::Overflow::Clip;
let rect = clamp_rect_to_canvas(lb.rect, canvas_cols).unwrap_or(lb.rect);
let own_clip = match clip {
Some(c) => match intersect_rects(c, rect) {
Some(r) => r,
None => return,
},
None => rect,
};
if own_clip.width == 0 || own_clip.height == 0 {
for child in sorted_children(lb) {
if !matches!(child.positioning.position, Position::Static) {
paint_box(child, arena, canvas, parent_bg, None);
}
}
return;
}
let own_paint_clip = if overflow_clips {
own_clip
} else {
clip.unwrap_or_else(|| {
Rect::new(0, 0, canvas_cols, canvas.layout.len() / canvas_cols.max(1))
})
};
let my_bg = match &lb.kind {
BoxKind::Normal => arena[lb.node_id].css_attr.get_background(),
BoxKind::Replaced(_) => None,
BoxKind::Anonymous => None,
};
let visibility_hidden = matches!(
arena[lb.node_id].css_attr.get_visibility(),
Visibility::Hidden
);
if !visibility_hidden {
match &lb.kind {
BoxKind::Normal => {
paint_block(lb, arena, canvas, my_bg.as_ref(), parent_bg, own_paint_clip);
}
BoxKind::Replaced(ReplacedContent::Image { cols, rows, .. }) => {
paint_image(lb, canvas, *cols, *rows, own_clip);
}
BoxKind::Replaced(ReplacedContent::Input) => {
paint_input(lb, arena, canvas, own_clip);
}
BoxKind::Replaced(ReplacedContent::Select) => {
paint_select(lb, arena, canvas, own_clip);
}
BoxKind::Replaced(ReplacedContent::Textarea) => {
paint_textarea(lb, arena, canvas, own_clip);
}
BoxKind::Replaced(ReplacedContent::Button) => {
paint_button(lb, arena, canvas, own_clip);
}
BoxKind::Anonymous => {
}
}
}
let effective_bg = if arena[lb.node_id].css_attr.background_clips_to_text() {
parent_bg
} else {
my_bg
.as_ref()
.map(|background| BackgroundFill { background, rect })
.or(parent_bg)
};
let edges = &lb.paint_style.box_edges;
let content_area = Rect::new(
rect.x
.saturating_add(edges.border.left.thickness())
.saturating_add(edges.padding.left),
rect.y
.saturating_add(edges.border.top.thickness())
.saturating_add(edges.padding.top),
rect.width.saturating_sub(edges.inner_horizontal()),
rect.height.saturating_sub(edges.inner_vertical()),
);
let children_clip = if overflow_clips {
Some(intersect_rects(own_clip, content_area).unwrap_or(Rect::new(0, 0, 0, 0)))
} else {
clip
};
paint_children(lb, arena, canvas, effective_bg, children_clip);
paint_scrollbar(lb, arena, canvas);
}
fn paint_scrollbar(lb: &LayoutBox, arena: &NodeArena, canvas: &mut PaintCanvas<'_>) {
if !canvas
.scrollbars_visible
.get(&lb.node_id)
.copied()
.unwrap_or(false)
{
return;
}
let Some(scroll) = lb.paint_style.scrollbar else {
return;
};
let fg = arena
.get(lb.node_id)
.and_then(|node| node.css_attr.get_color());
let dim = Some(fg.unwrap_or(crossterm::style::Color::DarkGrey));
if let Some(track) = clamp_rect_to_canvas(scroll.track, canvas.columns as usize) {
for y in track.y..track.y.saturating_add(track.height) {
let inside_thumb =
y >= scroll.thumb.y && y < scroll.thumb.y.saturating_add(scroll.thumb.height);
put(
canvas,
cell_index(canvas, track.x, y),
Some(if inside_thumb { '\u{2588}' } else { '\u{2502}' }),
dim,
None,
);
}
}
if let Some(h_track) = clamp_rect_to_canvas(scroll.h_track, canvas.columns as usize)
&& h_track.height > 0
{
for x in h_track.x..h_track.x.saturating_add(h_track.width) {
let inside_thumb =
x >= scroll.h_thumb.x && x < scroll.h_thumb.x.saturating_add(scroll.h_thumb.width);
put(
canvas,
cell_index(canvas, x, h_track.y),
Some(if inside_thumb { '\u{2588}' } else { '\u{2500}' }),
dim,
None,
);
}
}
}
fn paint_children(
lb: &LayoutBox,
arena: &NodeArena,
canvas: &mut PaintCanvas<'_>,
effective_bg: Option<BackgroundFill<'_>>,
children_clip: Option<Rect>,
) {
for child in sorted_children(lb) {
let clip = match child.positioning.position {
Position::Static => children_clip,
_ => None,
};
paint_box(child, arena, canvas, effective_bg, clip);
}
}
fn sorted_children(lb: &LayoutBox) -> SortedChildren<'_> {
let first = lb.children.first().map(stacking_key);
let needs_sort = lb.children.iter().any(|c| Some(stacking_key(c)) != first);
if needs_sort {
let mut children: Vec<&LayoutBox> = lb.children.iter().collect();
children.sort_by_key(|c| stacking_key(c));
SortedChildren::Sorted(children.into_iter())
} else {
SortedChildren::Straight(lb.children.iter())
}
}
pub(crate) fn stacking_key(c: &LayoutBox) -> (i32, u8) {
(
c.positioning.z_index.unwrap_or(0),
!matches!(c.positioning.position, Position::Static) as u8,
)
}
enum SortedChildren<'a> {
Straight(std::slice::Iter<'a, LayoutBox>),
Sorted(std::vec::IntoIter<&'a LayoutBox>),
}
impl<'a> Iterator for SortedChildren<'a> {
type Item = &'a LayoutBox;
fn next(&mut self) -> Option<Self::Item> {
match self {
SortedChildren::Straight(it) => it.next(),
SortedChildren::Sorted(it) => it.next(),
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
match self {
SortedChildren::Straight(it) => it.size_hint(),
SortedChildren::Sorted(it) => it.size_hint(),
}
}
}