use crossterm::terminal;
use crate::{
graphics::{Block, FontStyle, ImageCache, TermLayout},
html::css_parse::Overflow,
layout::{
BoxConstraint, Cursor, LayoutBox, Rect,
inner_formatting_context::flow::layout_tree_with_scroll_offsets,
layoutbox::{pin_fixed_boxes, pin_sticky_boxes},
paint::{paint_layout_tree, paint_layout_tree_region},
},
tdom::node::{NodeArena, NodeId, NodeType},
};
pub const MAX_TERM_COLS: u16 = 1000;
pub const MAX_TERM_ROWS: u16 = 500;
#[derive(Debug)]
pub struct TermMap {
pub columns: u16,
pub rows: u16,
}
impl TermMap {
pub fn get_term_map() -> std::io::Result<TermMap> {
let (raw_c, raw_r) = terminal::size()?;
let columns = if raw_c == 0 {
80
} else {
raw_c.clamp(1, MAX_TERM_COLS)
};
let rows = if raw_r == 0 {
24
} else {
raw_r.clamp(1, MAX_TERM_ROWS)
};
if raw_c != columns || raw_r != rows {
log::warn!(
"终端尺寸 raw {}x{} 越界,已夹紧为 {}x{}",
raw_c,
raw_r,
columns,
rows
);
}
log::info!("终端尺寸: {}x{}", columns, rows);
Ok(TermMap { columns, rows })
}
}
impl TermLayout {
pub fn new(map: &TermMap, zoom: f32) -> TermLayout {
let viewport_cells = (map.columns as usize)
.saturating_mul(map.rows as usize)
.min(MAX_TERM_COLS as usize * MAX_TERM_ROWS as usize);
let layout = vec![Block::default(); viewport_cells];
TermLayout {
layout,
last_frame: Vec::new(),
columns: map.columns,
rows: map.rows,
scroll_y: 0,
rendered_rows: 0,
zoom,
image_cache: ImageCache::new(),
input_states: std::collections::HashMap::new(),
scroll_offsets: std::collections::HashMap::new(),
scroll_x_offsets: std::collections::HashMap::new(),
open_select: None,
scrollbars_visible: std::collections::HashMap::new(),
}
}
}
impl TermLayout {
pub fn draw(
&mut self,
arena: &NodeArena,
root_id: NodeId,
term_map: &TermMap,
) -> Vec<LayoutBox> {
self.reset_canvas();
let boxes = self.layout_only(arena, root_id, term_map);
self.ensure_capacity(arena, &boxes);
paint_layout_tree(&boxes, arena, self);
boxes
}
pub fn layout_only(
&mut self,
arena: &NodeArena,
root_id: NodeId,
term_map: &TermMap,
) -> Vec<LayoutBox> {
self.input_states.retain(|_, st| st.focused);
let root = &arena[root_id];
assert_eq!(root.node_type, NodeType::Document);
let containing = BoxConstraint::root(term_map);
let mut cursor = Cursor::new();
let mut boxes = layout_tree_with_scroll_offsets(
arena,
root_id,
containing,
&mut cursor,
term_map.columns as usize,
term_map.rows as usize,
&self.scroll_offsets,
&self.scroll_x_offsets,
);
let viewport = Rect::new(
0,
self.scroll_y as usize,
term_map.columns as usize,
term_map.rows as usize,
);
pin_fixed_boxes(&mut boxes, viewport);
pin_sticky_boxes(&mut boxes, viewport);
boxes
}
pub fn repaint_region(&mut self, arena: &NodeArena, boxes: &[LayoutBox], region: Rect) {
self.ensure_capacity(arena, boxes);
self.clear_region(region);
paint_layout_tree_region(boxes, arena, self, region);
}
pub fn repaint_from(&mut self, arena: &NodeArena, boxes: &[LayoutBox]) {
self.reset_canvas();
self.ensure_capacity(arena, boxes);
paint_layout_tree(boxes, arena, self);
}
fn ensure_capacity(&mut self, arena: &NodeArena, boxes: &[LayoutBox]) {
fn walk(arena: &NodeArena, boxes: &[LayoutBox], bottom: &mut usize) {
for b in boxes {
*bottom = (*bottom).max(b.rect.y.saturating_add(b.rect.height.max(1)));
if arena[b.node_id].css_attr.get_overflow() == Overflow::Clip {
for child in &b.children {
if !matches!(
child.positioning.position,
crate::html::css_parse::Position::Static
) {
walk(arena, std::slice::from_ref(child), bottom);
}
}
continue;
}
walk(arena, &b.children, bottom);
}
}
let mut bottom = self.rows as usize;
walk(arena, boxes, &mut bottom);
let cols = self.columns as usize;
let viewport = cols.saturating_mul(self.rows as usize);
let needed = bottom.saturating_mul(cols).max(viewport);
if needed > self.layout.len() {
self.layout.resize(needed, Block::default());
} else if needed < self.layout.len() / 4 {
self.layout.truncate(needed);
self.layout.shrink_to_fit();
}
}
fn reset_canvas(&mut self) {
for cell in self.layout.iter_mut() {
blank_cell(cell);
}
self.rendered_rows = 0;
}
fn clear_region(&mut self, region: Rect) {
let cols = self.columns as usize;
let max = self.layout.len();
let x_start = region.x.min(cols);
let x_end = region.x.saturating_add(region.width).min(cols);
let y_end = region.y.saturating_add(region.height);
for y in region.y..y_end {
let row_start = y.saturating_mul(cols);
if row_start >= max {
break;
}
for x in x_start..x_end {
let idx = row_start + x;
if idx >= max {
break;
}
blank_cell(&mut self.layout[idx]);
}
}
}
}
fn blank_cell(cell: &mut Block) {
cell.word.clear();
cell.word.push(' ');
cell.fcolor = None;
cell.bcolor = None;
cell.style = FontStyle::empty();
}