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::{HitMap, SharedGeometry, SharedHitMap};
use crate::html::css_parse::Viewport;
use crate::html::css_parse::{DynamicState, PseudoClass, css_pseudo_rule_set};
use crate::html::html_parse::ParsedDocument;
use crate::source::SharedSourceText;
use crate::tdom::node::NodeId;
use crate::tui::graph_api::{DrainedBatch, InputState, RenderCmd, SharedScrollbarVisibility};
use super::GeometrySender;
use super::pipeline::{combined_css_source, flush_frame, parse_css_source, term_map_or_none};
use super::state::RenderState;
pub(super) fn render_thread(
rx: mpsc::Receiver<RenderCmd>,
document: ParsedDocument,
session_css: SharedSourceText,
max_scroll: Arc<AtomicU32>,
geo_tx: GeometrySender,
selection_text: Arc<Mutex<String>>,
scrollbars_visible: SharedScrollbarVisibility,
render_beat: Arc<AtomicU64>,
live_geo: super::LiveGeoSlot,
config: Config,
) {
let mut out = BufWriter::new(stdout());
let document_css = Arc::clone(&document.css);
let css_rules = Arc::new(parse_css_source(&combined_css_source(
&document_css,
&session_css,
)));
let pseudo_rules = css_pseudo_rule_set(&css_rules);
let mut state = RenderState {
document,
session_css,
document_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,
scrollbars_visible,
max_scroll,
live_geo,
};
let mut last_frame_at: Option<Instant> = None;
'frame: 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 cmd in batch.drain_scrolls() {
match cmd {
RenderCmd::RenderScroll { node_id, scroll_y } => {
let Some(term_map) = term_map_or_none() else {
break 'frame;
};
state.render_scroll(&mut out, &geo_tx, &term_map, node_id, scroll_y);
}
RenderCmd::RenderScrollX { node_id, scroll_x } => {
let Some(term_map) = term_map_or_none() else {
break 'frame;
};
state.render_scroll_x(&mut out, &geo_tx, &term_map, node_id, scroll_x);
}
_ => {}
}
}
for sel_cmd in batch.drain_selects() {
state.handle_select(&mut out, sel_cmd);
}
for css_cmd in batch.drain_css_reloads() {
if let RenderCmd::ReloadCss { css } = css_cmd {
let Some(term_map) = term_map_or_none() else {
break 'frame;
};
state.reload_css(&mut out, &geo_tx, &term_map, css);
}
}
render_beat.fetch_add(1, 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();
'snapshot: {
match cmd {
RenderCmd::Quit => break 'frame,
RenderCmd::Rerender { scroll_y } => {
{
let Some(tl) = state.term_layout.as_mut() else {
break 'snapshot;
};
let prev = tl.scroll_y;
tl.scroll_y = scroll_y;
tl.clamp_scroll();
if tl.scroll_y == prev {
break 'snapshot;
}
}
if state.has_pinned {
state.pin_fixed_and_repaint(&geo_tx);
}
let Some(tl) = state.term_layout.as_mut() else {
break 'snapshot;
};
tl.render(&mut out);
flush_frame(&mut out);
}
RenderCmd::Redraw { scroll_y, zoom } => {
let Some(term_map) = term_map_or_none() else {
break 'snapshot;
};
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 Some(term_map) = term_map_or_none() else {
break 'snapshot;
};
state.render_arena(
&mut out,
&geo_tx,
&term_map,
arena,
root_id,
scroll_y,
zoom,
dirty,
state_changed,
);
}
RenderCmd::RenderDocument {
arena,
root_id,
document_css,
scroll_y,
zoom,
dirty,
} => {
let Some(term_map) = term_map_or_none() else {
break 'snapshot;
};
state.render_document(
&mut out,
&geo_tx,
&term_map,
arena,
root_id,
document_css,
scroll_y,
zoom,
dirty,
);
}
RenderCmd::Hover { node_id } => {
if !apply_hover(&mut state, &mut out, node_id) {
break 'snapshot;
}
}
RenderCmd::Focus { node_id } => {
if !apply_focus(&mut state, &mut out, node_id) {
break 'snapshot;
}
}
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::RenderScroll { .. } | RenderCmd::RenderScrollX { .. } => {
log::debug!("Scroll command reached the snapshot queue; dropped");
}
RenderCmd::ReloadCss { .. } => {}
RenderCmd::FocusRequest { .. } | RenderCmd::BlurRequest { .. } => {
log::debug!("Focus request reached the render thread — dropping");
}
RenderCmd::ScrollAnimate { .. }
| RenderCmd::ScrollSet { .. }
| RenderCmd::ScrollIntoView { .. } => {
log::debug!("Scroll marker 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() {
apply_hover(&mut state, &mut out, node_id);
}
if let Some(RenderCmd::Focus { node_id }) = batch.take_focus() {
apply_focus(&mut state, &mut out, node_id);
}
}
}
fn apply_hover(
state: &mut RenderState,
out: &mut BufWriter<std::io::Stdout>,
node_id: Option<NodeId>,
) -> bool {
let prev = std::mem::replace(&mut state.dynamic.hovered, node_id);
if !state.pseudo_rules.contains_pseudo(PseudoClass::Hover) {
return false;
}
state.repaint_dynamic(out, PseudoClass::Hover, prev, node_id);
true
}
fn apply_focus(
state: &mut RenderState,
out: &mut BufWriter<std::io::Stdout>,
node_id: Option<NodeId>,
) -> bool {
let prev = std::mem::replace(&mut state.dynamic.focused, node_id);
if !state.pseudo_rules.contains_pseudo(PseudoClass::Focus) {
return false;
}
state.repaint_dynamic(out, PseudoClass::Focus, prev, node_id);
true
}
pub(super) fn hit_test_thread(rx: mpsc::Receiver<SharedGeometry>, hit_map: SharedHitMap) {
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: &mut InputState,
cmds: &mut Vec<RenderCmd>,
hovered: &mut Option<NodeId>,
viewport: Viewport,
ev: crate::script::ScriptEvent,
) {
*hovered = ev.hovered;
cmds.extend(ev.cmds);
if let Some(d) = ev.dirty {
let document_reloaded = d.document_css.is_some();
if document_reloaded {
state.reset_document_state(cmds);
}
if let Some(document_css) = d.document_css {
cmds.push(RenderCmd::RenderDocument {
arena: d.arena,
root_id: d.root_id,
document_css,
scroll_y: state.scroll_y,
zoom: state.zoom,
dirty: d.dirty,
});
} else {
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,
viewport,
},
dirty: d.dirty,
});
}
}
}