use std::collections::HashMap;
use crate::render::md::{self, Block, DocBlock, DocStreamSession};
use crate::render::rich::Span;
use crate::render::{RichLine, RichText};
use crate::theme::TokenSet;
use super::super::markdown::{BlockTypesetter, Row};
use super::item::{ItemBlock, RowCap, SharedDrawFn};
pub(super) enum EntryKind {
Static(Vec<ItemBlock>),
Stream(Box<StreamEntry>),
}
pub(super) struct StreamEntry {
pub(super) raw: String,
pub(super) session: DocStreamSession,
pub(super) closed_seen: usize,
pub(super) finished: bool,
}
pub(super) enum Segment {
Rows(Vec<Row>),
Custom { draw: SharedDrawFn, height: i32 },
}
impl Segment {
pub(super) fn height(&self) -> i32 {
match self {
Segment::Rows(rows) => rows.len() as i32,
Segment::Custom { height, .. } => *height,
}
}
}
pub(super) struct Entry {
pub(super) kind: EntryKind,
pub(super) segments: Vec<Segment>,
pub(super) height: i32,
}
impl Entry {
fn recount(&mut self) {
self.height = self.segments.iter().map(Segment::height).sum();
}
}
pub(super) struct FeedInner {
pub(super) entries: Vec<Entry>,
pub(super) index: HashMap<String, usize>,
pub(super) width: i32,
pub(super) prefix: Vec<i32>,
pub(super) gap: i32,
pub(super) tokens: Option<TokenSet>,
pub(super) fixup_scheduled: bool,
pub(super) blocks_typeset: u64,
pub(super) mutations: u64,
}
impl FeedInner {
pub(super) fn new() -> FeedInner {
FeedInner {
entries: Vec::new(),
index: HashMap::new(),
width: 0,
prefix: Vec::new(),
gap: 1,
tokens: None,
fixup_scheduled: false,
blocks_typeset: 0,
mutations: 0,
}
}
pub(super) fn total_rows(&self) -> i32 {
match self.entries.len() {
0 => 0,
n => self.prefix[n - 1] + self.entries[n - 1].height,
}
}
pub(super) fn rebuild_prefix_from(&mut self, start: usize) {
self.prefix.truncate(start);
let mut acc = if start == 0 {
0
} else {
self.prefix[start - 1] + self.entries[start - 1].height + self.gap
};
for e in &self.entries[start..] {
self.prefix.push(acc);
acc += e.height + self.gap;
}
}
pub(super) fn typeset_entry(&mut self, i: usize, full: bool) {
let (width, Some(tokens)) = (self.width, self.tokens) else {
return;
};
if width <= 0 {
return;
}
let ts = BlockTypesetter::new(&tokens);
let entry = &mut self.entries[i];
match &mut entry.kind {
EntryKind::Static(blocks) => {
if full || entry.segments.is_empty() {
self.blocks_typeset += blocks.len() as u64;
entry.segments = typeset_static(blocks, &ts, &tokens, width);
entry.recount();
}
}
EntryKind::Stream(stream) => {
if full {
let mut s = DocStreamSession::new(ts.styles().clone());
s.append(&stream.raw);
if stream.finished {
s.finish();
}
stream.session = s;
stream.closed_seen = 0;
entry.segments = vec![Segment::Rows(Vec::new()), Segment::Rows(Vec::new())];
}
if entry.segments.is_empty() {
entry.segments = vec![Segment::Rows(Vec::new()), Segment::Rows(Vec::new())];
}
let closed = stream.session.closed_blocks();
if stream.closed_seen < closed.len() {
let Segment::Rows(rows) = &mut entry.segments[0] else {
unreachable!("stream segment 0 is rows");
};
for b in &closed[stream.closed_seen..] {
self.blocks_typeset += 1;
ts.push_doc_block(rows, b, width, true);
}
stream.closed_seen = closed.len();
}
let closed_rows = match &entry.segments[0] {
Segment::Rows(rows) => rows.len(),
_ => 0,
};
let open = stream.session.open_blocks();
let mut rows: Vec<Row> = Vec::new();
for (bi, b) in open.iter().enumerate() {
self.blocks_typeset += 1;
if bi == 0 && closed_rows > 0 && doc_block_separates(b) {
rows.push(Row::plain(RichLine::new()));
}
ts.push_doc_block(&mut rows, b, width, bi > 0);
}
entry.segments[1] = Segment::Rows(rows);
entry.recount();
}
}
}
pub(super) fn retypeset_all(&mut self) {
for i in 0..self.entries.len() {
self.typeset_entry(i, true);
}
self.rebuild_prefix_from(0);
}
}
fn doc_block_separates(b: &DocBlock) -> bool {
match b {
DocBlock::Core(core) => !matches!(core, Block::ListItem { .. }),
DocBlock::Table(_) | DocBlock::Image(_) => true,
DocBlock::Task(_) => false,
#[allow(unreachable_patterns)]
_ => false,
}
}
fn typeset_static(
blocks: &[ItemBlock],
ts: &BlockTypesetter,
tokens: &TokenSet,
width: i32,
) -> Vec<Segment> {
let mut segments: Vec<Segment> = Vec::new();
let mut current: Vec<Row> = Vec::new();
let mut any_content = false;
for b in blocks {
match b {
ItemBlock::Text { text, cap } => {
if any_content && current.is_empty() {
current.push(Row::plain(RichLine::new()));
}
let ink = crate::render::Style::new().fg(tokens.text);
let lines = RichText::plain(text, ink).wrap(width.max(4)).lines;
push_capped(&mut current, lines, cap.as_ref(), tokens);
any_content = true;
}
ItemBlock::Rich { text, cap } => {
if any_content && current.is_empty() {
current.push(Row::plain(RichLine::new()));
}
let lines = text.wrap(width.max(4)).lines;
push_capped(&mut current, lines, cap.as_ref(), tokens);
any_content = true;
}
ItemBlock::Markdown(src) => {
if any_content && current.is_empty() {
current.push(Row::plain(RichLine::new()));
}
for block in md::parse_doc(src, ts.styles()) {
ts.push_doc_block(&mut current, &block, width, true);
}
any_content = true;
}
ItemBlock::Code { lang, source } => {
if any_content && current.is_empty() {
current.push(Row::plain(RichLine::new()));
}
let block = Block::CodeFence {
lang: lang.clone(),
lines: source.split('\n').map(str::to_string).collect(),
};
ts.push_block(&mut current, &block, width, true);
any_content = true;
}
ItemBlock::Custom(c) => {
if !current.is_empty() {
segments.push(Segment::Rows(std::mem::take(&mut current)));
}
if any_content {
segments.push(Segment::Rows(vec![Row::plain(RichLine::new())]));
}
segments.push(Segment::Custom {
draw: c.draw.clone(),
height: (c.height)(width).max(0),
});
any_content = true;
}
}
}
if !current.is_empty() {
segments.push(Segment::Rows(current));
}
segments
}
fn push_capped(
current: &mut Vec<Row>,
lines: Vec<RichLine>,
cap: Option<&RowCap>,
tokens: &TokenSet,
) {
let max = cap.and_then(|c| c.max_rows).map(|m| m.max(1));
match max {
Some(max) if lines.len() > max => {
let shown = max - 1;
let hidden = lines.len() - shown;
for line in lines.into_iter().take(shown) {
current.push(Row::plain(line));
}
let text = match cap.and_then(|c| c.marker.as_ref()) {
Some(f) => f(hidden),
None => format!("… (+{hidden} more lines)"),
};
current.push(Row::plain(RichLine::from_spans(vec![Span::new(
text,
crate::render::Style::new().fg(tokens.text_muted),
)])));
}
_ => {
for line in lines {
current.push(Row::plain(line));
}
}
}
}