use std::cell::RefCell;
use std::rc::Rc;
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, MouseButton, MouseKind, Phase, UiEvent};
use super::markdown::md_styles;
#[path = "feed_item.rs"]
mod item;
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, 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.entries.push(Entry {
key: key.clone(),
kind: EntryKind::Static(item.blocks),
segments: Vec::new(),
height: 0,
});
inner.index.insert(key, i);
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.entries.push(Entry {
key: key.clone(),
kind,
segments: Vec::new(),
height: 0,
});
inner.index.insert(key, i);
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))
}
pub fn item_at_row(&self, row: i32) -> Option<(String, i32)> {
let inner = self.inner.borrow();
if row < 0 || inner.entries.is_empty() {
return None;
}
let i = inner
.prefix
.partition_point(|&p| p <= row)
.saturating_sub(1);
let top = *inner.prefix.get(i)?;
let entry = inner.entries.get(i)?;
let within = row - top;
if within < 0 || within >= entry.height {
return None; }
Some((entry.key.clone(), within))
}
fn sync_extent(&self) -> bool {
let total = self.inner.borrow().total_rows();
match self.rows.try_get_untracked() {
Some(cur) if cur != total => {
self.rows.set(total);
true
}
_ => false,
}
}
fn bump_version(&self) {
if self.version.try_get_untracked().is_some() {
self.version.update(|v| *v += 1);
}
}
fn publish(&self) {
self.sync_extent();
self.bump_version();
}
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;
if state.sync_extent() {
state.bump_version();
}
});
}
}
type ItemPressFn = Box<dyn FnMut(&str, i32)>;
pub struct Feed {
state: FeedState,
gap: i32,
selected: Option<Signal<Option<String>>>,
on_item_press: Option<ItemPressFn>,
layout: Option<LayoutStyle>,
rule: crate::widgets::MdRuleStyle,
}
impl Feed {
pub fn new(state: &FeedState) -> Feed {
Feed {
state: state.clone(),
gap: 1,
selected: None,
on_item_press: None,
layout: None,
rule: crate::widgets::MdRuleStyle::default(),
}
}
pub fn rule_style(mut self, rule: crate::widgets::MdRuleStyle) -> Feed {
self.rule = rule;
self
}
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 on_item_press(mut self, f: impl FnMut(&str, i32) + 'static) -> Feed {
self.on_item_press = Some(Box::new(f));
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.rule != self.rule {
inner.tokens = Some(*t);
inner.rule = self.rule;
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(base.height(Dimension::Auto));
let measure_state = state.clone();
el = el.measure(move |avail| {
if avail.w <= 1 {
return crate::base::Size::ZERO;
}
let mut inner = measure_state.inner.borrow_mut();
if inner.width != avail.w {
inner.width = avail.w;
inner.retypeset_all();
}
let total = inner.total_rows();
drop(inner);
if measure_state.rows.try_get_untracked() != Some(total) {
measure_state.schedule_geometry_sync();
}
crate::base::Size::new(avail.w, total)
});
} else {
el = el.style(style);
}
let _ = rows;
if let Some(mut press) = self.on_item_press {
let press_state = state.clone();
el = el.on(Phase::Bubble, move |ctx, ev| {
if let UiEvent::Mouse(m) = ev {
if matches!(m.kind, MouseKind::Down(MouseButton::Left)) {
let row = m.pos.y - ctx.current_rect().y;
if let Some((key, within)) = press_state.item_at_row(row) {
press(&key, within);
ctx.stop_propagation();
}
}
}
});
}
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()
},
))
}
}
#[path = "feed_draw.rs"]
mod draw;
use draw::draw_feed;
#[cfg(test)]
#[path = "feed_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "feed_rich_tests.rs"]
mod rich_tests;
#[cfg(test)]
#[path = "feed_press_tests.rs"]
mod press_tests;