use std::io::{BufWriter, Write};
use std::sync::atomic::AtomicU32;
use std::sync::{Arc, Mutex};
use compact_str::CompactString;
use crossterm::style::Color;
use crate::events::SharedGeometry;
use crate::html::css_parse::Viewport;
use crate::html::css_parse::{self, CssRule, DynamicState, PseudoClassSet};
use crate::html::html_parse::ParsedDocument;
use crate::layout::Rect;
use crate::layout::layoutbox::geometry::{
SelectRow, overlay_options, select_item_indices, select_overlay_rect, select_rows,
};
use crate::source::SharedSourceText;
use crate::tdom::node::{NodeArena, NodeId};
use crate::tui::graph_api::{
InputRenderState, RenderCmd, SelectionMode, SharedScrollbarVisibility, TermLayout,
};
use super::LiveGeoSlot;
use super::overlay::apply_select_overlay;
use super::pipeline::flush_frame;
use super::region::{find_box_rect, union_rect};
use super::selection::{highlight_lines, lines_bounds, selection_lines};
pub(super) fn cascade_document(
document: &ParsedDocument,
css_rules: &CssRule,
viewport: Viewport,
) -> (NodeId, NodeArena) {
let mut arena = (*document.arena).clone();
css_parse::reset_css_attr(&mut arena, document.root_id);
css_parse::css_into_node_with_state(
&mut arena,
document.root_id,
css_rules,
&DynamicState {
viewport,
..Default::default()
},
);
(document.root_id, arena)
}
pub(super) const SELECTION_BG: Color = Color::Rgb {
r: 0x3b,
g: 0x42,
b: 0x61,
};
pub(super) const OVERLAY_BG: Color = Color::Rgb {
r: 0x24,
g: 0x28,
b: 0x3b,
};
pub(super) const OVERLAY_FG: Color = Color::Rgb {
r: 0xc0,
g: 0xca,
b: 0xf5,
};
pub(super) const GROUP_FG: Color = Color::Rgb {
r: 0x7a,
g: 0x83,
b: 0xa8,
};
#[derive(Debug, Clone, Copy)]
pub(super) struct TextSel {
pub(super) mode: SelectionMode,
pub(super) anchor: (u32, u32),
pub(super) end: (u32, u32),
}
pub(super) struct OpenSelect {
pub(super) node_id: NodeId,
pub(super) filter: Option<CompactString>,
pub(super) rect: Rect,
pub(super) options: Vec<crate::layout::layoutbox::geometry::SelectOption>,
pub(super) rows: Vec<crate::layout::layoutbox::geometry::SelectRow>,
pub(super) highlight: usize,
}
impl OpenSelect {
pub(super) fn highlight_row(&self) -> usize {
self.rows
.iter()
.position(|r| matches!(r, SelectRow::Item(i) if *i == self.highlight))
.unwrap_or(0)
}
pub(super) fn reanchor(
&mut self,
arena: &NodeArena,
boxes: &[crate::layout::LayoutBox],
) -> bool {
let Some(select_rect) = find_box_rect(boxes, self.node_id) else {
return false;
};
let options = overlay_options(arena, self.node_id, self.filter.as_deref());
let rows = select_rows(&options);
let item_count = select_item_indices(&rows).len();
self.highlight = self.highlight.min(item_count.saturating_sub(1));
self.rect = select_overlay_rect(&options, &rows, select_rect);
self.options = options;
self.rows = rows;
true
}
}
pub(super) struct RenderState {
pub(super) document: ParsedDocument,
pub(super) session_css: SharedSourceText,
pub(super) document_css: SharedSourceText,
pub(super) extra_css: String,
pub(super) css_rules: Arc<CssRule>,
pub(super) pseudo_rules: PseudoClassSet,
pub(super) term_layout: Option<TermLayout>,
pub(super) title_set: bool,
pub(super) dynamic: DynamicState,
pub(super) geometry: Option<SharedGeometry>,
pub(super) has_pinned: bool,
pub(super) text_selection: Option<TextSel>,
pub(super) selection_text: Arc<Mutex<String>>,
pub(super) open_select: Option<OpenSelect>,
pub(super) scrollbars_visible: SharedScrollbarVisibility,
pub(super) max_scroll: Arc<AtomicU32>,
pub(super) live_geo: LiveGeoSlot,
}
impl RenderState {
pub(super) fn handle_input<W: Write>(
&mut self,
out: &mut BufWriter<W>,
node_id: NodeId,
value: CompactString,
cursor: usize,
focused: bool,
selection: Option<(usize, usize)>,
) {
let Some(tl) = self.term_layout.as_mut() else {
return;
};
let Some(geo) = self.geometry.as_ref() else {
return;
};
tl.input_states.insert(
node_id,
InputRenderState {
value,
cursor,
focused,
selection,
},
);
let rect = find_box_rect(&geo.boxes, node_id);
match rect {
Some(r) => tl.repaint_region(&geo.arena, &geo.boxes, r),
None => tl.repaint_from(&geo.arena, &geo.boxes),
}
tl.render(out);
flush_frame(out);
}
pub(super) fn handle_select<W: Write>(&mut self, out: &mut BufWriter<W>, cmd: RenderCmd) {
match cmd {
RenderCmd::SelectOpen { node_id, filter } => {
let Some(geo) = self.geometry.as_ref() else {
return;
};
if let Some(prev) = self.open_select.take()
&& prev.node_id != node_id
&& let Some(tl) = self.term_layout.as_mut()
{
tl.open_select = None;
let prev_select = find_box_rect(&geo.boxes, prev.node_id).unwrap_or(prev.rect);
tl.repaint_region(&geo.arena, &geo.boxes, union_rect(prev_select, prev.rect));
}
let options = overlay_options(&geo.arena, node_id, filter.as_deref());
let rows = select_rows(&options);
let selected = match filter {
Some(_) => 0,
None => {
let value = geo
.arena
.get(node_id)
.and_then(|n| n.attributes.get("value"))
.cloned()
.unwrap_or_default();
options.iter().position(|o| o.value == value).unwrap_or(0)
}
};
let Some(select_rect) = find_box_rect(&geo.boxes, node_id) else {
return;
};
let rect = select_overlay_rect(&options, &rows, select_rect);
self.open_select = Some(OpenSelect {
node_id,
filter,
rect,
options,
rows,
highlight: selected,
});
let Some(tl) = self.term_layout.as_mut() else {
return;
};
tl.open_select = Some(node_id);
tl.repaint_region(&geo.arena, &geo.boxes, union_rect(select_rect, rect));
if let Some(open) = &self.open_select {
apply_select_overlay(tl, open);
}
tl.render(out);
flush_frame(out);
}
RenderCmd::SelectClose { .. } => {
let Some(open) = self.open_select.take() else {
return;
};
let Some(tl) = self.term_layout.as_mut() else {
return;
};
tl.open_select = None;
let Some(geo) = self.geometry.as_ref() else {
return;
};
let select_rect = find_box_rect(&geo.boxes, open.node_id).unwrap_or(open.rect);
tl.repaint_region(&geo.arena, &geo.boxes, union_rect(select_rect, open.rect));
tl.render(out);
flush_frame(out);
}
RenderCmd::SelectMove { node_id, highlight } => {
let Some(open) = self.open_select.as_mut() else {
return;
};
if open.node_id != node_id {
return;
}
let item_count = select_item_indices(&open.rows).len();
open.highlight = highlight.min(item_count.saturating_sub(1));
let Some(tl) = self.term_layout.as_mut() else {
return;
};
let Some(geo) = self.geometry.as_ref() else {
return;
};
tl.repaint_region(&geo.arena, &geo.boxes, open.rect);
apply_select_overlay(tl, open);
tl.render(out);
flush_frame(out);
}
_ => {}
}
}
pub(super) fn apply_text_selection_cmd<W: Write>(
&mut self,
out: &mut BufWriter<W>,
anchor: Option<(u32, u32)>,
end: Option<(u32, u32)>,
mode: SelectionMode,
) {
let new_sel = match (anchor, end) {
(Some(a), Some(b)) => Some(TextSel {
mode,
anchor: a,
end: b,
}),
_ => None,
};
let prev = self.text_selection;
self.text_selection = new_sel;
let Some(tl) = self.term_layout.as_mut() else {
return;
};
let Some(geo) = self.geometry.as_ref() else {
return;
};
let cols = tl.columns as u32;
let prev_lines = match &prev {
Some(n) => highlight_lines(&geo.arena, &geo.boxes, n, cols),
None => Vec::new(),
};
let new_lines = match &new_sel {
Some(n) => highlight_lines(&geo.arena, &geo.boxes, n, cols),
None => Vec::new(),
};
let region = match (lines_bounds(&prev_lines), lines_bounds(&new_lines)) {
(Some(a), Some(b)) => Some(union_rect(a, b)),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
};
if let Some(r) = region {
tl.repaint_region(&geo.arena, &geo.boxes, r);
tl.apply_text_selection(&new_lines, SELECTION_BG);
}
tl.render(out);
flush_frame(out);
let raw_lines = match &new_sel {
Some(n) => crate::layout::clip_flow_lines_scoped(
&geo.boxes,
n.anchor,
n.end,
&selection_lines(n, cols),
),
None => Vec::new(),
};
let text = match &new_sel {
Some(_) => crate::layout::extract_flow_text(&geo.arena, &geo.boxes, &raw_lines),
None => String::new(),
};
if let Ok(mut guard) = self.selection_text.lock() {
*guard = text;
}
}
}