use std::io::{BufWriter, stdout};
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use crate::config::Config;
use crate::events::{Geometry, HitMap};
use crate::html::css_parse::{DynamicState, PseudoClass, css_pseudo_rule_set};
use crate::layout::layout::TermMap;
use crate::tdom::node::{NodeArena, NodeId};
use crate::tui::graph_api::{DrainedBatch, InputState, RenderCmd};
use super::pipeline::{flush_frame, parse_css_source};
use super::state::RenderState;
pub(super) fn render_thread(
rx: mpsc::Receiver<RenderCmd>,
html: Arc<str>,
base_css: Arc<str>,
max_scroll: Arc<AtomicU32>,
geo_tx: Option<mpsc::Sender<Arc<Geometry>>>,
selection_text: Arc<Mutex<String>>,
render_beat: Arc<AtomicU64>,
config: Config,
) {
let mut out = BufWriter::new(stdout());
let css_rules = Arc::new(parse_css_source(&base_css));
let pseudo_rules = css_pseudo_rule_set(&css_rules);
let mut state = RenderState {
html,
base_css,
extra_css: String::new(),
css_rules,
pseudo_rules,
term_layout: None,
title_set: false,
geometry: None,
has_pinned: false,
dynamic: DynamicState::default(),
text_selection: None,
selection_text,
open_select: None,
max_scroll,
};
let mut last_frame_at: Option<Instant> = None;
loop {
let first = match rx.recv() {
Ok(c) => c,
Err(e) => {
log::error!("Render channel disconnected: {}", e);
break;
}
};
if let Some(t) = last_frame_at {
let since = t.elapsed();
if since < config.frame_gap {
std::thread::sleep(config.frame_gap - since);
}
}
let mut batch = DrainedBatch::new();
batch.absorb(first);
while !batch.quit_requested() {
match rx.try_recv() {
Ok(next) => batch.absorb(next),
Err(_) => break,
}
}
for cmd in batch.drain_inputs() {
if let RenderCmd::RenderInput {
node_id,
value,
cursor,
focused,
selection,
} = cmd
{
state.handle_input(&mut out, node_id, value, cursor, focused, selection);
}
}
for sel_cmd in batch.drain_selects() {
state.handle_select(&mut out, sel_cmd);
}
render_beat.store(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64,
Ordering::Relaxed,
);
last_frame_at = Some(Instant::now());
if let Some(RenderCmd::TextSelection { anchor, end, mode }) = batch.take_text_selection() {
state.apply_text_selection_cmd(&mut out, anchor, end, mode);
}
if let Some(cmd) = batch.take_snapshot() {
let batch_t0 = Instant::now();
match cmd {
RenderCmd::Quit => break,
RenderCmd::Rerender { scroll_y } => {
{
let Some(tl) = state.term_layout.as_mut() else {
continue;
};
let prev = tl.scroll_y;
tl.scroll_y = scroll_y;
tl.clamp_scroll();
if tl.scroll_y == prev {
continue;
}
}
if state.has_pinned {
state.pin_fixed_and_repaint(&geo_tx);
}
let Some(tl) = state.term_layout.as_mut() else {
continue;
};
tl.render(&mut out);
flush_frame(&mut out);
}
RenderCmd::Redraw { scroll_y, zoom } => {
let term_map = match TermMap::get_ter_map() {
Ok(m) => m,
Err(e) => {
log::error!("Failed to get terminal size: {}", e);
continue;
}
};
state.redraw(&mut out, &geo_tx, &term_map, scroll_y, zoom);
}
RenderCmd::RenderArena {
arena,
root_id,
scroll_y,
zoom,
dynamic,
dirty,
} => {
let state_changed = state.dynamic != dynamic;
state.dynamic = dynamic;
let term_map = match TermMap::get_ter_map() {
Ok(m) => m,
Err(e) => {
log::error!("Failed to get terminal size: {}", e);
continue;
}
};
state.render_arena(
&mut out,
&geo_tx,
&term_map,
&arena,
root_id,
scroll_y,
zoom,
dirty,
state_changed,
);
}
RenderCmd::RenderHtml { html } => {
let term_map = match TermMap::get_ter_map() {
Ok(m) => m,
Err(e) => {
log::error!("Failed to get terminal size: {}", e);
continue;
}
};
state.render_html(&mut out, &geo_tx, &term_map, html);
}
RenderCmd::Hover { node_id } => {
let prev = state.dynamic.hovered;
state.dynamic.hovered = node_id;
if !state.pseudo_rules.contains_pseudo(PseudoClass::Hover) {
continue;
}
state.repaint_dynamic(&mut out, PseudoClass::Hover, prev, node_id);
}
RenderCmd::Focus { node_id } => {
let prev = state.dynamic.focused;
state.dynamic.focused = node_id;
if !state.pseudo_rules.contains_pseudo(PseudoClass::Focus) {
continue;
}
state.repaint_dynamic(&mut out, PseudoClass::Focus, prev, node_id);
}
RenderCmd::SelectOpen { .. }
| RenderCmd::SelectClose { .. }
| RenderCmd::SelectMove { .. } => {
state.handle_select(&mut out, cmd);
}
RenderCmd::TextSelection { anchor, end, mode } => {
state.apply_text_selection_cmd(&mut out, anchor, end, mode);
}
RenderCmd::RenderInput {
node_id,
value,
cursor,
focused,
selection,
} => {
state.handle_input(&mut out, node_id, value, cursor, focused, selection);
}
RenderCmd::FocusRequest { .. } | RenderCmd::BlurRequest { .. } => {
log::debug!("Focus request reached the render thread — dropping");
}
}
let elapsed = batch_t0.elapsed();
if elapsed > Duration::from_millis(200) {
log::warn!("渲染线程单批命令处理耗时 {elapsed:?}");
}
}
if let Some(RenderCmd::Hover { node_id }) = batch.take_hover() {
let prev = state.dynamic.hovered;
state.dynamic.hovered = node_id;
if state.pseudo_rules.contains_pseudo(PseudoClass::Hover) {
state.repaint_dynamic(&mut out, PseudoClass::Hover, prev, node_id);
}
}
}
}
pub(super) fn hit_test_thread(rx: mpsc::Receiver<Arc<Geometry>>, hit_map: Arc<Mutex<Arc<HitMap>>>) {
while let Ok(geo) = rx.recv() {
let map = Arc::new(HitMap::build(&geo));
match hit_map.lock() {
Ok(mut guard) => *guard = map,
Err(e) => log::error!("Hit map lock poisoned: {}", e),
}
}
log::debug!("Hit-test thread exiting");
}
pub(super) fn fold_script_event(
state: &InputState,
cmds: &mut Vec<RenderCmd>,
hovered: &mut Option<NodeId>,
live_dom: &mut Option<(Arc<NodeArena>, NodeId)>,
ev: crate::script::ScriptEvent,
) {
*hovered = ev.hovered;
cmds.extend(ev.cmds);
if let Some(d) = ev.dirty {
*live_dom = Some((Arc::clone(&d.arena), d.root_id));
cmds.push(RenderCmd::RenderArena {
arena: d.arena,
root_id: d.root_id,
scroll_y: state.scroll_y,
zoom: state.zoom,
dynamic: DynamicState {
hovered: ev.hovered,
focused: state.focused_input,
},
dirty: d.dirty,
});
}
}