1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
//! Feed windowed painter (child module of `feed`, split for the
//! file-size discipline — one file, one task: this one turns the
//! typeset entries into pixels for the visible band; `feed.rs` owns
//! the public model, the state handle and the widget wiring).
//!
//! OWNER: CONTENT (app-widgets wave).
use crate::base::Rect;
use crate::theme::TokenSet;
use crate::ui::StyledCanvas;
use super::super::markdown::draw_rows;
use super::item::SharedDrawFn;
use super::typeset::Segment;
use super::FeedState;
/// Windowed paint: only entries intersecting `rect ∩ canvas bounds`
/// touch the canvas. Custom draws run after the state borrow releases
/// (they are app code).
pub(super) 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 {
// Width discovery / resize: re-typeset (pure cache fill;
// the reactive extent syncs via the deferred fixup).
inner.width = rect.w;
inner.retypeset_all();
drop(inner);
state.schedule_geometry_sync();
inner = state.inner.borrow_mut();
}
// The band is what can be SEEN, not what the surface is.
// `canvas.size()` was the proxy here and it over-reports: a
// ClippedCanvas delegates size() to the canvas it wraps, so a
// Feed inside a Scroll measured its band against the whole
// terminal and painted every row the screen could hold instead
// of the handful inside the viewport. Bounded waste (capped at
// screen height, not proportional to body length), but paid on
// every frame. `clip_rect()` answers the question the proxy was
// standing in for.
let bounds = canvas.clip_rect();
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; // exclusive
let inner = &*inner;
// Selection highlight: ground the selected item's band FIRST —
// rows paint over with transparent backgrounds, so item inks
// stay and the tint shows through (code-fence rows keep their
// own ground by design).
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,
);
}
}
// First entry whose span can reach the band (binary search on
// prefix starts, then step back one).
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 } => {
// The custom block gets its FULL rect (its
// top may sit above the band); the canvas
// clips.
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);
}
}