nandi/jolt-nativepublic Fork 0
d682bfa94c04bddb877d4b5e21ef938b826cc9be
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

paint.rs · 666 lines · 27.0 KBRust Blame HistoryRaw
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago1//! Drawing the tree into a grid of cells.
2//!
3//! One pass, top to bottom: each node is handed a rect by [`crate::layout`] and
4//! paints itself into it. Two things fall out of the walk and are kept —
5//! the focus ring, in the order the widgets were painted, and every focusable
6//! widget's rect, so a mouse click can be turned back into a node.
7//!
8//! Overlays are collected rather than drawn in place: a floating panel belongs
9//! over the whole screen, so it is painted after everything else at the size it
10//! asked for, in the middle.
11
12use crate::layout::{self, wrap, Align};
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago13use crate::graphics;
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago14use crate::screen::{self, attr, Color, Rect, Screen, Style};
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago15use crate::tree::{Props, Tag, Tree};
16
17const SPINNER: [char; 8] = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧'];
18
19/// What one frame of painting learned about the tree, for the input half to
20/// use on the next key or click.
21#[derive(Clone, Debug, Default)]
22pub struct Painted {
23 /// Focusable nodes in paint order — the order Tab walks.
24 pub ring: Vec<u32>,
25 /// Where each of them ended up.
26 pub hits: Vec<(u32, Rect)>,
27 /// How far each scroll node's viewport actually was, after clamping to the
28 /// content it had. Written back so a caller cannot scroll past the end.
Let a backlog keep its place, and leave room for what is under it 68910bd nandi 17d ago29 /// Each `:scroll` painted, as (node, the offset it was painted at, the
Scroll the list under the pointer, and from where it actually is a785201 nandi 16d ago30 /// furthest it could have been, and the area it was painted into). The
31 /// second number is what tells a caller whether it is at the bottom, which
32 /// is what sticking to it means; the rect is what a wheel is aimed at.
33 pub scrolled: Vec<(u32, u16, u16, Rect)>,
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago34 /// Where the cursor should sit — the focused entry's caret, if any.
35 pub cursor: Option<(u16, u16)>,
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago36 /// The pictures this frame wants on screen, in the cells they were given.
37 /// Nothing was painted for them: the grid has no pixels, and the terminal
38 /// is what draws one — see [`crate::graphics`].
39 pub images: Vec<graphics::Placement>,
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago40}
41
Measure a node once, and paint only what is on screen c41903b nandi 16d ago42impl Painted {
43 /// Move everything down by `rows`.
44 ///
45 /// A scroll paints its content into a buffer that starts partway down the
46 /// column, so what came back is in that buffer's coordinates. This puts it
47 /// back into the content's, where the viewport's own offset means what it
48 /// says.
49 fn shift_down(&mut self, rows: u16) {
50 if rows == 0 {
51 return;
52 }
53 for (_, rect) in &mut self.hits {
54 rect.y = rect.y.saturating_add(rows);
55 }
56 for (_, _, _, rect) in &mut self.scrolled {
57 rect.y = rect.y.saturating_add(rows);
58 }
59 for placement in &mut self.images {
60 placement.area.y = placement.area.y.saturating_add(rows);
61 }
62 if let Some((_, y)) = &mut self.cursor {
63 *y = y.saturating_add(rows);
64 }
65 }
66}
67
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago68struct Painter<'a> {
69 tree: &'a Tree,
70 screen: &'a mut Screen,
71 focus: u32,
72 /// Where the caret sits in the focused entry's text, in characters.
73 caret: usize,
74 tick: u64,
75 out: Painted,
76 overlays: Vec<u32>,
77}
78
79/// Paint the whole tree. `focus` is the node the ring is currently on and
80/// `tick` advances the spinners.
81pub fn frame(tree: &Tree, screen: &mut Screen, focus: u32, caret: usize, tick: u64) -> Painted {
82 screen.clear();
83 let mut painter = Painter {
84 tree,
85 screen,
86 focus,
87 caret,
88 tick,
89 out: Painted::default(),
90 overlays: Vec::new(),
91 };
92 let area = painter.screen.rect();
93 painter.node(tree.root(), area, Style::default(), true);
94
95 // Overlays float above the rest, so they are painted after it — and a
96 // click landing on one must beat a click on whatever it covers, which is
97 // what putting their hit rects first does.
98 let overlays = std::mem::take(&mut painter.overlays);
99 let below = std::mem::take(&mut painter.out.hits);
100 for id in overlays {
101 painter.overlay(id, area);
102 }
103 painter.out.hits.extend(below);
104 painter.out
105}
106
107impl Painter<'_> {
108 fn style_for(&self, props: &Props, inherited: Style, enabled: bool) -> Style {
109 let mut style = inherited;
110 if let Some(fg) = Color::parse(props.str("color")) {
111 style.fg = fg;
112 }
113 if let Some(bg) = Color::parse(props.str("bg")) {
114 style.bg = bg;
115 }
116 for (key, bit) in [
117 ("bold", attr::BOLD),
118 ("dim", attr::DIM),
119 ("underline", attr::UNDERLINE),
120 ("reverse", attr::REVERSE),
121 ("blink", attr::BLINK),
122 ("italic", attr::ITALIC),
123 ] {
124 if props.bool(key, false) {
125 style.attrs |= bit;
126 }
127 }
128 if !enabled {
129 // `:sensitive false` dims the widget *and its whole subtree*, which
130 // is what it means in every other glimmer backend.
131 style.attrs |= attr::DIM;
132 }
133 style
134 }
135
136 fn node(&mut self, id: u32, area: Rect, inherited: Style, enabled: bool) {
137 if area.is_empty() || !self.tree.exists(id) {
138 return;
139 }
140 let tag = self.tree.tag(id);
141 let props = self.tree.props(id);
142 let enabled = enabled && props.bool("sensitive", true);
143 let style = self.style_for(&props, inherited, enabled);
144 if props.has("bg") {
145 self.screen.fill(area, style);
146 }
147 if enabled && tag.focusable() {
148 self.out.ring.push(id);
149 self.out.hits.push((id, area));
150 }
151
152 let pad = layout::inset(&tag, &props);
153 let inner = area.shrink(pad);
154 match tag {
155 Tag::Overlay => self.overlays.push(id),
156 Tag::Frame => {
157 self.border(area, props.label(), style);
158 self.children(id, inner, style, enabled);
159 }
160 Tag::Scroll => self.scroll(id, inner, style, enabled),
Share a column's rows out at the width it has, not at its own height 5acc801 nandi 17d ago161 Tag::Box | Tag::Window => self.children(id, inner, style, enabled),
162 // A tag this backend has not learned paints as a vertical box, so
163 // whatever is under it still reaches the screen. When there is
164 // nothing under it, its own text does instead: an unknown *leaf*
165 // is a widget the caller has and this has not — frq's `:status`
166 // badge, its `:link` — and painting the box and not the label is
167 // the one outcome that loses the text altogether. A link vanishing
168 // out of the middle of a message is not a missing widget; it is a
169 // missing sentence.
170 Tag::Unknown(_) => {
Let a backlog keep its place, and leave room for what is under it 68910bd nandi 17d ago171 if self.tree.child_count(id) == 0 && !props.has("src") && !props.has("feed") {
Share a column's rows out at the width it has, not at its own height 5acc801 nandi 17d ago172 self.wrapped(inner, props.label(), style);
173 } else {
174 self.children(id, inner, style, enabled);
175 }
176 }
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago177 Tag::Label => self.wrapped(inner, props.label(), style),
178 Tag::Title => self.wrapped(inner, props.label(), style.with(attr::BOLD)),
179 Tag::DimLabel => self.wrapped(inner, props.label(), style.with(attr::DIM)),
180 Tag::Button => self.button(id, inner, &props, style),
181 Tag::CheckButton => self.check(id, inner, &props, style),
182 Tag::Entry => self.entry(id, inner, &props, style),
183 Tag::Separator => self.separator(inner, style),
184 Tag::Progress => self.progress(inner, &props, style),
185 Tag::Spinner => {
186 let ch = SPINNER[(self.tick as usize) % SPINNER.len()];
187 self.screen.set(inner.x, inner.y, ch, style);
188 }
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago189 Tag::Image => self.image(id, inner, &props, style),
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago190 Tag::Reaction => self.reaction(id, inner, &props, style),
191 // The same glyph with nothing around it: a character in a line,
192 // and the line is what says anything about it.
193 Tag::Emoji => {
194 self.screen
195 .text(inner.x, inner.y, inner.w, props.str("emoji"), style);
196 }
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago197 Tag::Listbox => self.listbox(id, inner, &props, style, enabled),
198 // A spacer is the absence of anything; the clear at the top of the
199 // frame has already drawn it.
200 Tag::Spacer => {}
201 }
202 }
203
Measure a node once, and paint only what is on screen c41903b nandi 16d ago204 /// Put a subtree's focusable nodes into the ring without painting it.
205 ///
206 /// What a scroll owes the parts of its content it did not paint. Tab walks
207 /// the ring, and a reader tabbing onto a button below the fold is how they
208 /// scroll to it — so a widget being out of sight cannot take it out of the
209 /// order. It has no rect, which is exactly right: there is nowhere on the
210 /// screen to click something that is not on the screen.
211 fn ring_only(&mut self, id: u32, enabled: bool) {
212 if !self.tree.exists(id) {
213 return;
214 }
215 let enabled = enabled && self.tree.props_of(id).bool("sensitive", true);
216 if enabled && self.tree.tag_of(id).focusable() {
217 self.out.ring.push(id);
218 }
219 for child in self.tree.children_of(id) {
220 self.ring_only(*child, enabled);
221 }
222 }
223
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago224 fn children(&mut self, id: u32, area: Rect, style: Style, enabled: bool) {
225 if area.is_empty() {
226 return;
227 }
228 let rects = layout::children_rects(self.tree, id, area);
229 for (child, rect) in self.tree.children(id).into_iter().zip(rects) {
230 // Clip to the parent: a child asking for more rows than are left
231 // paints what fits rather than over its neighbours.
232 let bottom = area.y.saturating_add(area.h);
233 let right = area.x.saturating_add(area.w);
234 if rect.y >= bottom || rect.x >= right {
235 continue;
236 }
237 let clipped = Rect::new(
238 rect.x,
239 rect.y,
240 rect.w.min(right - rect.x),
241 rect.h.min(bottom - rect.y),
242 );
243 self.node(child, clipped, style, enabled);
244 }
245 }
246
247 fn wrapped(&mut self, area: Rect, text: &str, style: Style) {
248 for (i, line) in wrap(text, area.w).into_iter().enumerate() {
249 if i as u16 >= area.h {
250 break;
251 }
252 self.screen
253 .text(area.x, area.y + i as u16, area.w, &line, style);
254 }
255 }
256
257 fn focused(&self, id: u32) -> bool {
258 self.focus == id
259 }
260
261 fn button(&mut self, id: u32, area: Rect, props: &Props, style: Style) {
262 let mut style = match props.str("kind") {
263 "primary" => style.with(attr::BOLD),
264 "destructive" => style.fg(Color::parse("red").unwrap_or_default()),
265 _ => style,
266 };
267 if self.focused(id) {
268 style = style.with(attr::REVERSE);
269 }
270 let label = format!("[ {} ]", props.label());
271 self.screen.text(area.x, area.y, area.w, &label, style);
272 }
273
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago274 /// A picture: the cells it was given, and a note of where they are.
275 ///
276 /// Nothing goes in them. The terminal draws the picture over the blank
277 /// cells when the frame is flushed, which is the only way pixels reach a
278 /// grid; where there is no protocol for that, the cells carry the note
279 /// that says a picture is here, and the link above it is the way to it.
280 fn image(&mut self, id: u32, area: Rect, props: &Props, style: Style) {
281 let path = props.str("src");
282 if path.is_empty() || area.is_empty() {
283 return;
284 }
285 if !graphics::supported() {
286 self.screen
287 .text(area.x, area.y, area.w, layout::PICTURE, style.with(attr::DIM));
288 return;
289 }
290 // The column hands a child its whole width; a picture takes only what
291 // its shape asks for out of that, so the placement is the picture and
292 // not the room around it.
293 let (cols, rows) = layout::image_cells(props, area.w);
294 let area = Rect::new(area.x, area.y, cols.min(area.w), rows.min(area.h));
295 if area.is_empty() {
296 return;
297 }
298 self.out.images.push(graphics::Placement {
299 node: id,
300 path: path.to_owned(),
301 area,
302 crop_top: 0,
303 crop_bottom: 0,
304 });
305 }
306
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago307 /// A reaction pill: the glyph, the tally where there is one, and whether
308 /// you are on it.
309 ///
310 /// No border around it. A window draws a lozenge because it has half-cells
311 /// to draw one in; here brackets would cost two columns of a row that
312 /// already carries three chips, and would say "button" about a thing whose
313 /// whole picture is the glyph. Yours is bold, which is the one bit of the
314 /// pill a reader actually reads off it.
315 fn reaction(&mut self, id: u32, area: Rect, props: &Props, style: Style) {
316 let mut style = if props.bool("mine", false) {
317 style.with(attr::BOLD)
318 } else {
319 style
320 };
321 if self.focused(id) {
322 style = style.with(attr::REVERSE);
323 }
324 self.screen
325 .text(area.x, area.y, area.w, &layout::pill_text(props), style);
326 }
327
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago328 fn check(&mut self, id: u32, area: Rect, props: &Props, style: Style) {
329 let style = if self.focused(id) {
330 style.with(attr::REVERSE)
331 } else {
332 style
333 };
334 let mark = if props.bool("active", false) {
335 'x'
336 } else {
337 ' '
338 };
339 let label = format!("[{mark}] {}", props.label());
340 self.screen.text(area.x, area.y, area.w, &label, style);
341 }
342
343 fn entry(&mut self, id: u32, area: Rect, props: &Props, style: Style) {
344 let focused = self.focused(id);
345 let text = props.str("text");
346 let showing_placeholder = text.is_empty();
347 let shown = layout::entry_text(props);
348 let mut style = style.with(attr::UNDERLINE);
349 if showing_placeholder {
350 style = style.with(attr::DIM);
351 }
352 if focused {
353 style = style.with(attr::REVERSE);
354 }
355 // The field is its whole rect, not just the text in it: a reader needs
356 // to see where it can type before it has typed anything.
357 self.screen.fill(area, style);
358 let rows = area.h.max(1);
359 let lines = if props.cells("rows", 1) > 1 {
360 wrap(&shown, area.w)
361 } else {
362 vec![shown.chars().collect::<String>()]
363 };
364 let caret = self.caret.min(text.chars().count());
365 // A line longer than the field scrolls sideways to keep the caret in
366 // view — the end of it is where someone is usually typing, but not
367 // always, so it follows the caret rather than the end.
368 for (i, line) in lines.iter().take(rows as usize).enumerate() {
369 let len = line.chars().count();
370 let last = i + 1 == lines.len().min(rows as usize);
371 let window = area.w.saturating_sub(1).max(1) as usize;
372 let from = if last && !showing_placeholder {
373 caret.saturating_sub(window)
374 } else {
375 len.saturating_sub(window)
376 };
377 let visible: String = line.chars().skip(from).collect();
378 self.screen
379 .text(area.x, area.y + i as u16, area.w, &visible, style);
380 if focused && last {
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago381 // In columns rather than characters: an emoji typed into the
382 // line is two cells wide, and a caret counted in characters
383 // sits a column left of the text for each one.
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago384 let col = if showing_placeholder {
385 0
386 } else {
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago387 let typed: String = visible
388 .chars()
389 .take(caret.saturating_sub(from))
390 .collect();
391 (screen::text_cols(&typed)).min(area.w.saturating_sub(1))
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago392 };
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago393 self.out.cursor = Some((area.x.saturating_add(col), area.y + i as u16));
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago394 }
395 }
396 }
397
398 fn separator(&mut self, area: Rect, style: Style) {
399 for x in area.x..area.x.saturating_add(area.w) {
400 self.screen.set(x, area.y, '─', style);
401 }
402 }
403
404 fn progress(&mut self, area: Rect, props: &Props, style: Style) {
405 let value = props.num("value", 0.0).clamp(0.0, 1.0);
406 let filled = (value * area.w as f64).round() as u16;
407 for x in 0..area.w {
408 let ch = if x < filled { '█' } else { '░' };
409 self.screen.set(area.x + x, area.y, ch, style);
410 }
411 let label = props.label();
412 if !label.is_empty() {
413 let at = area.x + (area.w.saturating_sub(label.chars().count() as u16)) / 2;
414 self.screen.text(at, area.y, area.w, label, style);
415 }
416 }
417
418 fn listbox(&mut self, id: u32, area: Rect, props: &Props, style: Style, enabled: bool) {
419 let items = self.tree.children(id);
420 // No `:selected` at all means the cursor is on the first row: a list
421 // with no cursor cannot be moved with the arrows, and a caller that
422 // wants none says so with -1.
423 let selected = props.num("selected", 0.0);
424 let selected = if selected < 0.0 {
425 None
426 } else {
427 Some(selected as usize)
428 };
429 // Keep the cursor on screen: scroll only as far as it takes.
430 let rows = area.h as usize;
431 let first = match selected {
432 Some(sel) if rows > 0 && sel >= rows => sel + 1 - rows,
433 _ => 0,
434 };
435 for (row, item) in items.iter().skip(first).take(rows).enumerate() {
436 let y = area.y + row as u16;
437 let chosen = selected == Some(first + row);
438 let mut row_style = style;
439 if chosen {
440 row_style = row_style.with(if self.focused(id) {
441 attr::REVERSE
442 } else {
443 attr::BOLD
444 });
445 self.screen.fill(Rect::new(area.x, y, area.w, 1), row_style);
446 }
447 let marker = if chosen { "" } else { " " };
448 self.screen.text(area.x, y, area.w, marker, row_style);
449 let cell = Rect::new(area.x + 2, y, area.w.saturating_sub(2), 1);
450 self.node(*item, cell, row_style, enabled);
451 }
452 }
453
454 fn scroll(&mut self, id: u32, area: Rect, style: Style, enabled: bool) {
455 let props = self.tree.props(id);
456 // The content is painted at its full height into a screen of its own,
457 // then the visible window of it is copied across. Doing it this way
458 // means every widget inside a scroll paints exactly as it would
459 // outside one — nothing has to know it is being clipped.
460 let content_h = self
461 .tree
462 .children(id)
463 .iter()
464 .map(|c| layout::height_for_width(self.tree, *c, area.w))
465 .sum::<u16>()
466 .max(1);
467 let max_offset = content_h.saturating_sub(area.h);
468 let offset = props.cells("offset", 0).min(max_offset);
Scroll the list under the pointer, and from where it actually is a785201 nandi 16d ago469 self.out.scrolled.push((id, offset, max_offset, area));
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago470
Measure a node once, and paint only what is on screen c41903b nandi 16d ago471 // Only the part of the content the viewport is showing is painted.
472 // A backlog is a hundred messages and a screen holds a dozen; painting
473 // the whole column into a grid that tall and copying a window out of
474 // it costs the same on the ninetieth message nobody is looking at as
475 // on the one they are reading — which is what made scrolling a long
476 // conversation cost more than scrolling a short one.
477 //
478 // `band` is the rows worth painting: the visible window, grown to whole
479 // children at each end so that a message straddling an edge is laid out
480 // in one piece and cut by the copy rather than by the layout. Its top
481 // is where the buffer's row 0 is, and everything the pass below learned
482 // is in the buffer's coordinates — so it is moved back into the
483 // content's before the rest of this reads it against `offset`.
484 let full = Rect::new(0, 0, area.w, content_h);
485 let rects = layout::children_rects(self.tree, id, full);
486 let kids = self.tree.children(id);
487 let seen = offset..offset.saturating_add(area.h);
488 let mut base = seen.start;
489 let mut foot = seen.end.min(content_h);
490 // In order, and every child accounted for: the ones on screen are
491 // painted, and the ones that are not still take their place in the
492 // focus ring below.
493 let mut plan = Vec::with_capacity(kids.len());
494 for (child, rect) in kids.iter().zip(&rects) {
495 let shown = rect.y < seen.end && rect.y.saturating_add(rect.h) > seen.start;
496 if shown {
497 base = base.min(rect.y);
498 foot = foot.max(rect.y.saturating_add(rect.h));
499 }
500 plan.push((*child, *rect, shown));
501 }
502 let band = foot.saturating_sub(base).max(1);
503
504 let mut buffer = Screen::new(area.w, band);
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago505 let mut inner = Painter {
506 tree: self.tree,
507 screen: &mut buffer,
508 focus: self.focus,
509 caret: self.caret,
510 tick: self.tick,
511 out: Painted::default(),
512 overlays: Vec::new(),
513 };
Measure a node once, and paint only what is on screen c41903b nandi 16d ago514 for (child, rect, shown) in plan {
515 if !shown {
516 inner.ring_only(child, enabled);
517 continue;
518 }
519 // The same clip `children` applies, against the content rather than
520 // the band: a child asking for more than the column has paints what
521 // fits. Nothing is clipped to the band itself — a child hanging off
522 // either end of it is what the copy below is for.
523 let width = rect.w.min(full.w.saturating_sub(rect.x));
524 inner.node(
525 child,
526 Rect::new(rect.x, rect.y - base, width, rect.h),
527 style,
528 enabled,
529 );
530 }
531 let mut learned = inner.out;
532 learned.shift_down(base);
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago533
534 for y in 0..area.h {
535 for x in 0..area.w {
Measure a node once, and paint only what is on screen c41903b nandi 16d ago536 if let Some(cell) = buffer.cell(x, (y + offset).saturating_sub(base)) {
Keep a glyph whole, selector and all, and copy it across whole 3dd441e nandi 16d ago537 self.screen.put(area.x + x, area.y + y, cell.clone());
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago538 }
539 }
540 }
541 // Widgets inside keep their place in the focus ring; their rects move
542 // by the viewport, and the ones scrolled out of sight take no clicks.
543 self.out.ring.extend(learned.ring);
544 for (node, rect) in learned.hits {
545 if rect.y >= offset && rect.y < offset.saturating_add(area.h) {
546 self.out.hits.push((
547 node,
548 Rect::new(
549 area.x + rect.x,
550 area.y + rect.y - offset,
551 rect.w,
552 rect.h.min(area.h),
553 ),
554 ));
555 }
556 }
Scroll the list under the pointer, and from where it actually is a785201 nandi 16d ago557 // A scroll inside this one was painted into the buffer, so its area is
558 // in the buffer's coordinates: move it the way the hits above moved,
559 // and drop the ones the viewport is not showing. A wheel over a nested
560 // list has to land on the list under the pointer, and a rect left in
561 // the wrong space is a wheel aimed at whatever happens to be there.
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago562 // A picture inside a scroll moves with it, and is cut off by the
563 // viewport rather than painted over what is above or below: the
564 // protocol crops from the source, so a backlog scrolls past a picture
565 // a row at a time instead of losing it whole at the edge.
566 let bottom = offset.saturating_add(area.h);
567 for mut placement in learned.images {
568 let top = placement.area.y;
569 let foot = top.saturating_add(placement.area.h);
570 let seen_top = top.max(offset);
571 let seen_foot = foot.min(bottom);
572 if seen_foot <= seen_top {
573 continue;
574 }
575 placement.crop_top += seen_top - top;
576 placement.crop_bottom += foot - seen_foot;
577 placement.area = Rect::new(
578 area.x + placement.area.x,
579 area.y + seen_top - offset,
580 placement.area.w,
581 seen_foot - seen_top,
582 );
583 self.out.images.push(placement);
584 }
Scroll the list under the pointer, and from where it actually is a785201 nandi 16d ago585 for (node, inner_offset, max, rect) in learned.scrolled {
586 if rect.y >= offset && rect.y < offset.saturating_add(area.h) {
587 self.out.scrolled.push((
588 node,
589 inner_offset,
590 max,
591 Rect::new(
592 area.x + rect.x,
593 area.y + rect.y - offset,
594 rect.w,
595 rect.h.min(area.h),
596 ),
597 ));
598 }
599 }
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago600 if let Some((cx, cy)) = learned.cursor {
601 if cy >= offset && cy < offset.saturating_add(area.h) {
602 self.out.cursor = Some((area.x + cx, area.y + cy - offset));
603 }
604 }
605 }
606
607 fn overlay(&mut self, id: u32, screen: Rect) {
608 let props = self.tree.props(id);
609 let w = layout::width(self.tree, id, false).min(screen.w);
610 let h = layout::height_for_width(self.tree, id, w).min(screen.h);
611 let (x, y) = (
612 screen.x + Align::Center.offset_pub(w, screen.w),
613 screen.y + Align::Center.offset_pub(h, screen.h),
614 );
615 let area = Rect::new(x, y, w, h);
616 let style = self.style_for(&props, Style::default(), true);
617 // Blank what is under it: a floating panel that shows the screen
618 // through its gaps is unreadable.
619 for row in area.y..area.y + area.h {
620 for col in area.x..area.x + area.w {
621 self.screen.set(col, row, ' ', style);
622 }
623 }
624 self.border(area, props.label(), style);
625 let pad = layout::inset(&Tag::Overlay, &props);
626 self.children(id, area.shrink(pad), style, true);
627 }
628
629 /// A single-line box, with `label` set into the top edge when there is one.
630 fn border(&mut self, area: Rect, label: &str, style: Style) {
631 if area.w < 2 || area.h < 2 {
632 return;
633 }
634 let (x1, y1) = (area.x + area.w - 1, area.y + area.h - 1);
635 for x in area.x..=x1 {
636 self.screen.set(x, area.y, '─', style);
637 self.screen.set(x, y1, '─', style);
638 }
639 for y in area.y..=y1 {
640 self.screen.set(area.x, y, '│', style);
641 self.screen.set(x1, y, '│', style);
642 }
643 self.screen.set(area.x, area.y, '┌', style);
644 self.screen.set(x1, area.y, '┐', style);
645 self.screen.set(area.x, y1, '└', style);
646 self.screen.set(x1, y1, '┘', style);
647 if !label.is_empty() && area.w > 4 {
648 let text = format!(" {label} ");
649 self.screen.text(
650 area.x + 1,
651 area.y,
652 area.w - 2,
653 &text,
654 style.with(attr::BOLD),
655 );
656 }
657 }
658}
659
660impl Align {
661 /// [`Align::offset`] is private to the layout module; overlays are the one
662 /// caller outside it that centres something by hand.
663 fn offset_pub(self, size: u16, avail: u16) -> u16 {
664 layout::place(self, size, avail).0
665 }
666}