use std::cell::RefCell;
use std::rc::Rc;
use crate::base::Rect;
use crate::layout::{Dimension, Style as LayoutStyle};
use crate::reactive::{Scope, Signal};
use crate::render::md::StreamSession;
use crate::theme::TokenSet;
use crate::ui::{dyn_view, Element, StyledCanvas};
use super::markdown::{draw_rows, md_styles};
pub enum FeedBlock {
Text(String),
Markdown(String),
Code {
lang: String,
source: String,
},
Custom(CustomBlock),
}
type SharedDrawFn = Rc<dyn Fn(&mut dyn StyledCanvas, Rect)>;
pub struct CustomBlock {
height: Box<dyn Fn(i32) -> i32>,
draw: SharedDrawFn,
}
impl CustomBlock {
pub fn new(
height: impl Fn(i32) -> i32 + 'static,
draw: impl Fn(&mut dyn StyledCanvas, Rect) + 'static,
) -> CustomBlock {
CustomBlock {
height: Box::new(height),
draw: Rc::new(draw),
}
}
}
pub struct FeedItem {
blocks: Vec<FeedBlock>,
}
impl FeedItem {
pub fn new() -> FeedItem {
FeedItem { blocks: Vec::new() }
}
pub fn markdown(src: impl Into<String>) -> FeedItem {
FeedItem::new().block(FeedBlock::Markdown(src.into()))
}
pub fn text(s: impl Into<String>) -> FeedItem {
FeedItem::new().block(FeedBlock::Text(s.into()))
}
pub fn code(lang: impl Into<String>, source: impl Into<String>) -> FeedItem {
FeedItem::new().block(FeedBlock::Code {
lang: lang.into(),
source: source.into(),
})
}
pub fn block(mut self, b: FeedBlock) -> FeedItem {
self.blocks.push(b);
self
}
}
impl Default for FeedItem {
fn default() -> Self {
FeedItem::new()
}
}
#[path = "feed_typeset.rs"]
mod typeset;
use typeset::{Entry, EntryKind, FeedInner, Segment, StreamEntry};
#[derive(Clone)]
pub struct FeedState {
inner: Rc<RefCell<FeedInner>>,
version: Signal<u64>,
rows: Signal<i32>,
}
impl FeedState {
pub fn new(cx: Scope) -> FeedState {
FeedState {
inner: Rc::new(RefCell::new(FeedInner::new())),
version: cx.signal(0u64),
rows: cx.signal(0i32),
}
}
pub fn push(&self, key: impl Into<String>, item: FeedItem) {
let key = key.into();
{
let mut inner = self.inner.borrow_mut();
let existing = inner.index.get(&key).copied();
if let Some(i) = existing {
drop(inner);
self.update_at(i, EntryKind::Static(item.blocks));
return;
}
let i = inner.entries.len();
inner.index.insert(key, i);
inner.entries.push(Entry {
kind: EntryKind::Static(item.blocks),
segments: Vec::new(),
height: 0,
});
inner.typeset_entry(i, true);
inner.rebuild_prefix_from(i);
}
self.publish();
}
pub fn update(&self, key: &str, item: FeedItem) -> bool {
let Some(i) = self.inner.borrow().index.get(key).copied() else {
return false;
};
self.update_at(i, EntryKind::Static(item.blocks));
true
}
fn update_at(&self, i: usize, kind: EntryKind) {
{
let mut inner = self.inner.borrow_mut();
inner.entries[i].kind = kind;
inner.entries[i].segments = Vec::new();
inner.typeset_entry(i, true);
inner.rebuild_prefix_from(i);
}
self.publish();
}
pub fn push_stream(&self, key: impl Into<String>) {
let key = key.into();
{
let mut inner = self.inner.borrow_mut();
let styles = inner.tokens.as_ref().map(md_styles).unwrap_or_default();
let kind = EntryKind::Stream(Box::new(StreamEntry {
raw: String::new(),
session: StreamSession::new(styles),
closed_seen: 0,
finished: false,
}));
let existing = inner.index.get(&key).copied();
let i = match existing {
Some(i) => {
inner.entries[i].kind = kind;
inner.entries[i].segments = Vec::new();
i
}
None => {
let i = inner.entries.len();
inner.index.insert(key, i);
inner.entries.push(Entry {
kind,
segments: Vec::new(),
height: 0,
});
i
}
};
inner.typeset_entry(i, true);
inner.rebuild_prefix_from(i);
}
self.publish();
}
pub fn stream_append(&self, key: &str, delta: &str) -> bool {
{
let mut inner = self.inner.borrow_mut();
let Some(i) = inner.index.get(key).copied() else {
return false;
};
let EntryKind::Stream(stream) = &mut inner.entries[i].kind else {
return false;
};
stream.raw.push_str(delta);
stream.session.append(delta);
inner.typeset_entry(i, false);
inner.rebuild_prefix_from(i);
}
self.publish();
true
}
pub fn stream_finish(&self, key: &str) -> bool {
{
let mut inner = self.inner.borrow_mut();
let Some(i) = inner.index.get(key).copied() else {
return false;
};
let EntryKind::Stream(stream) = &mut inner.entries[i].kind else {
return false;
};
stream.session.finish();
stream.finished = true;
inner.typeset_entry(i, false);
inner.rebuild_prefix_from(i);
}
self.publish();
true
}
pub fn clear(&self) {
{
let mut inner = self.inner.borrow_mut();
inner.entries.clear();
inner.index.clear();
inner.prefix.clear();
}
self.publish();
}
pub fn len(&self) -> usize {
self.inner.borrow().entries.len()
}
pub fn is_empty(&self) -> bool {
self.inner.borrow().entries.is_empty()
}
pub fn total_rows(&self) -> Signal<i32> {
self.rows
}
pub fn blocks_typeset_total(&self) -> u64 {
self.inner.borrow().blocks_typeset
}
fn publish(&self) {
let total = self.inner.borrow().total_rows();
if let Some(cur) = self.rows.try_get_untracked() {
if cur != total {
self.rows.set(total);
}
}
if self.version.try_get_untracked().is_some() {
self.version.update(|v| *v += 1);
}
}
fn schedule_geometry_sync(&self) {
let mut inner = self.inner.borrow_mut();
if inner.fixup_scheduled {
return;
}
inner.fixup_scheduled = true;
drop(inner);
let state = self.clone();
crate::reactive::after(std::time::Duration::ZERO, move || {
state.inner.borrow_mut().fixup_scheduled = false;
state.publish();
});
}
}
pub struct Feed {
state: FeedState,
gap: i32,
layout: Option<LayoutStyle>,
}
impl Feed {
pub fn new(state: &FeedState) -> Feed {
Feed {
state: state.clone(),
gap: 1,
layout: None,
}
}
pub fn gap(mut self, rows: i32) -> Feed {
self.gap = rows.max(0);
self
}
pub fn layout(mut self, layout: LayoutStyle) -> Feed {
self.layout = Some(layout);
self
}
pub fn view(self, cx: Scope) -> crate::ui::View {
let t = crate::widgets::theme_tokens(cx);
self.element(cx, &t).build()
}
pub fn element(self, _cx: Scope, t: &TokenSet) -> Element {
let state = self.state;
{
let mut inner = state.inner.borrow_mut();
if inner.tokens != Some(*t) {
inner.tokens = Some(*t);
inner.gap = self.gap;
inner.retypeset_all();
} else if inner.gap != self.gap {
inner.gap = self.gap;
inner.rebuild_prefix_from(0);
}
}
state.publish();
let rows = state.rows;
let len_state = state.clone();
let content_sized = self.layout.is_none();
let style = match self.layout {
Some(explicit) => {
let mut el_style = explicit;
el_style.overflow = crate::layout::Overflow::Clip;
el_style
}
None => LayoutStyle::default().width(Dimension::Percent(1.0)),
};
let tokens = *t;
let mut el = Element::new()
.role(crate::ui::Role::List)
.access_value(move || format!("{} items", len_state.len()));
if content_sized {
let base = style.clone();
el = el.style_signal(move || {
let mut s = base.clone();
s.height = Dimension::Cells(rows.get().max(1));
s
});
} else {
el = el.style(style);
}
let version = state.version;
el.child(dyn_view(
LayoutStyle::default()
.width(Dimension::Percent(1.0))
.height(Dimension::Percent(1.0)),
move || {
version.get(); let state = state.clone();
Element::new()
.style(
LayoutStyle::default()
.width(Dimension::Percent(1.0))
.height(Dimension::Percent(1.0)),
)
.draw(move |canvas, rect| draw_feed(&state, &tokens, canvas, rect))
.build()
},
))
}
}
fn draw_feed(state: &FeedState, t: &TokenSet, canvas: &mut dyn StyledCanvas, rect: Rect) {
let mut customs: Vec<(SharedDrawFn, Rect)> = Vec::new();
{
let mut inner = state.inner.borrow_mut();
if rect.w > 1 && inner.width != rect.w {
inner.width = rect.w;
inner.retypeset_all();
drop(inner);
state.schedule_geometry_sync();
inner = state.inner.borrow_mut();
}
let bounds = Rect::from_size(canvas.size());
let band = rect.intersect(bounds);
if band.is_empty() || inner.entries.is_empty() {
return;
}
let first_row = band.y - rect.y;
let last_row = first_row + band.h; let inner = &*inner;
let mut i = inner
.prefix
.partition_point(|&p| p <= first_row)
.saturating_sub(1);
while i < inner.entries.len() {
let top = inner.prefix[i];
if top >= last_row {
break;
}
let entry = &inner.entries[i];
let mut seg_top = top;
for seg in &entry.segments {
let h = seg.height();
if seg_top >= last_row {
break;
}
if seg_top + h > first_row {
match seg {
Segment::Rows(rows) => {
let skip = (first_row - seg_top).max(0) as usize;
let visible = ((last_row - seg_top).min(h) as usize).min(rows.len());
if skip < visible {
let y = rect.y + seg_top + skip as i32;
draw_rows(
canvas,
Rect::new(rect.x, y, rect.w, (visible - skip) as i32),
t,
&rows[skip..visible],
);
}
}
Segment::Custom { draw, height } => {
customs.push((
draw.clone(),
Rect::new(rect.x, rect.y + seg_top, rect.w, *height),
));
}
}
}
seg_top += h;
}
i += 1;
}
}
for (draw, r) in customs {
draw(canvas, r);
}
}
#[cfg(test)]
#[path = "feed_tests.rs"]
mod tests;