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::DocStreamSession;
use crate::theme::TokenSet;
use crate::ui::{dyn_view, Element, StyledCanvas};
use super::markdown::{draw_rows, md_styles};
#[path = "feed_item.rs"]
mod item;
use item::SharedDrawFn;
pub use item::{CustomBlock, FeedBlock, FeedItem};
#[path = "feed_sync.rs"]
mod sync;
pub use sync::SyncSpec;
#[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.mutations += 1;
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.mutations += 1;
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: DocStreamSession::new(styles),
closed_seen: 0,
finished: false,
}));
inner.mutations += 1;
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.mutations += 1;
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.mutations += 1;
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.mutations += 1;
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
}
pub(super) fn mutation_count(&self) -> u64 {
self.inner.borrow().mutations
}
pub fn row_of(&self, key: &str) -> Option<i32> {
let inner = self.inner.borrow();
let i = *inner.index.get(key)?;
Some(inner.prefix.get(i).copied().unwrap_or(0))
}
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,
selected: Option<Signal<Option<String>>>,
layout: Option<LayoutStyle>,
}
impl Feed {
pub fn new(state: &FeedState) -> Feed {
Feed {
state: state.clone(),
gap: 1,
selected: None,
layout: None,
}
}
pub fn selected_key(mut self, key: Signal<Option<String>>) -> Feed {
self.selected = Some(key);
self
}
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;
let selected = self.selected;
el.child(dyn_view(
LayoutStyle::default()
.width(Dimension::Percent(1.0))
.height(Dimension::Percent(1.0)),
move || {
version.get(); let sel = selected.and_then(|s| s.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, sel.as_deref(), canvas, rect)
})
.build()
},
))
}
}
fn draw_feed(
state: &FeedState,
t: &TokenSet,
selected: Option<&str>,
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;
if let Some(i) = selected.and_then(|k| inner.index.get(k).copied()) {
let top = inner.prefix[i];
let h = inner.entries[i].height;
if top < last_row && top + h > first_row {
canvas.fill(
Rect::new(rect.x, rect.y + top, rect.w, h),
' ',
t.selection_fg,
t.selection_bg,
);
}
}
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;
#[cfg(test)]
#[path = "feed_rich_tests.rs"]
mod rich_tests;