nandi/jolt-nativepublic Fork 0
e40b795acdf08fdae31fc3f95e29fcfc01e29eea
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 · 668 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
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago12use crate::graphics;
Run the formatter over the tree 3e8c6f0 nandi 13d ago13use crate::layout::{self, wrap, Align};
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 15d 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 15d 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() {
Run the formatter over the tree 3e8c6f0 nandi 13d ago286 self.screen.text(
287 area.x,
288 area.y,
289 area.w,
290 layout::PICTURE,
291 style.with(attr::DIM),
292 );
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago293 return;
294 }
295 // The column hands a child its whole width; a picture takes only what
296 // its shape asks for out of that, so the placement is the picture and
297 // not the room around it.
298 let (cols, rows) = layout::image_cells(props, area.w);
299 let area = Rect::new(area.x, area.y, cols.min(area.w), rows.min(area.h));
300 if area.is_empty() {
301 return;
302 }
303 self.out.images.push(graphics::Placement {
304 node: id,
305 path: path.to_owned(),
306 area,
307 crop_top: 0,
308 crop_bottom: 0,
309 });
310 }
311
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago312 /// A reaction pill: the glyph, the tally where there is one, and whether
313 /// you are on it.
314 ///
315 /// No border around it. A window draws a lozenge because it has half-cells
316 /// to draw one in; here brackets would cost two columns of a row that
317 /// already carries three chips, and would say "button" about a thing whose
318 /// whole picture is the glyph. Yours is bold, which is the one bit of the
319 /// pill a reader actually reads off it.
320 fn reaction(&mut self, id: u32, area: Rect, props: &Props, style: Style) {
321 let mut style = if props.bool("mine", false) {
322 style.with(attr::BOLD)
323 } else {
324 style
325 };
326 if self.focused(id) {
327 style = style.with(attr::REVERSE);
328 }
329 self.screen
330 .text(area.x, area.y, area.w, &layout::pill_text(props), style);
331 }
332
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago333 fn check(&mut self, id: u32, area: Rect, props: &Props, style: Style) {
334 let style = if self.focused(id) {
335 style.with(attr::REVERSE)
336 } else {
337 style
338 };
339 let mark = if props.bool("active", false) {
340 'x'
341 } else {
342 ' '
343 };
344 let label = format!("[{mark}] {}", props.label());
345 self.screen.text(area.x, area.y, area.w, &label, style);
346 }
347
348 fn entry(&mut self, id: u32, area: Rect, props: &Props, style: Style) {
349 let focused = self.focused(id);
350 let text = props.str("text");
351 let showing_placeholder = text.is_empty();
352 let shown = layout::entry_text(props);
353 let mut style = style.with(attr::UNDERLINE);
354 if showing_placeholder {
355 style = style.with(attr::DIM);
356 }
357 if focused {
358 style = style.with(attr::REVERSE);
359 }
360 // The field is its whole rect, not just the text in it: a reader needs
361 // to see where it can type before it has typed anything.
362 self.screen.fill(area, style);
363 let rows = area.h.max(1);
364 let lines = if props.cells("rows", 1) > 1 {
365 wrap(&shown, area.w)
366 } else {
367 vec![shown.chars().collect::<String>()]
368 };
369 let caret = self.caret.min(text.chars().count());
370 // A line longer than the field scrolls sideways to keep the caret in
371 // view — the end of it is where someone is usually typing, but not
372 // always, so it follows the caret rather than the end.
373 for (i, line) in lines.iter().take(rows as usize).enumerate() {
374 let len = line.chars().count();
375 let last = i + 1 == lines.len().min(rows as usize);
376 let window = area.w.saturating_sub(1).max(1) as usize;
377 let from = if last && !showing_placeholder {
378 caret.saturating_sub(window)
379 } else {
380 len.saturating_sub(window)
381 };
382 let visible: String = line.chars().skip(from).collect();
383 self.screen
384 .text(area.x, area.y + i as u16, area.w, &visible, style);
385 if focused && last {
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago386 // In columns rather than characters: an emoji typed into the
387 // line is two cells wide, and a caret counted in characters
388 // 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 ago389 let col = if showing_placeholder {
390 0
391 } else {
Run the formatter over the tree 3e8c6f0 nandi 13d ago392 let typed: String = visible.chars().take(caret.saturating_sub(from)).collect();
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago393 (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 ago394 };
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago395 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 ago396 }
397 }
398 }
399
400 fn separator(&mut self, area: Rect, style: Style) {
401 for x in area.x..area.x.saturating_add(area.w) {
402 self.screen.set(x, area.y, '─', style);
403 }
404 }
405
406 fn progress(&mut self, area: Rect, props: &Props, style: Style) {
407 let value = props.num("value", 0.0).clamp(0.0, 1.0);
408 let filled = (value * area.w as f64).round() as u16;
409 for x in 0..area.w {
410 let ch = if x < filled { '█' } else { '░' };
411 self.screen.set(area.x + x, area.y, ch, style);
412 }
413 let label = props.label();
414 if !label.is_empty() {
415 let at = area.x + (area.w.saturating_sub(label.chars().count() as u16)) / 2;
416 self.screen.text(at, area.y, area.w, label, style);
417 }
418 }
419
420 fn listbox(&mut self, id: u32, area: Rect, props: &Props, style: Style, enabled: bool) {
421 let items = self.tree.children(id);
422 // No `:selected` at all means the cursor is on the first row: a list
423 // with no cursor cannot be moved with the arrows, and a caller that
424 // wants none says so with -1.
425 let selected = props.num("selected", 0.0);
426 let selected = if selected < 0.0 {
427 None
428 } else {
429 Some(selected as usize)
430 };
431 // Keep the cursor on screen: scroll only as far as it takes.
432 let rows = area.h as usize;
433 let first = match selected {
434 Some(sel) if rows > 0 && sel >= rows => sel + 1 - rows,
435 _ => 0,
436 };
437 for (row, item) in items.iter().skip(first).take(rows).enumerate() {
438 let y = area.y + row as u16;
439 let chosen = selected == Some(first + row);
440 let mut row_style = style;
441 if chosen {
442 row_style = row_style.with(if self.focused(id) {
443 attr::REVERSE
444 } else {
445 attr::BOLD
446 });
447 self.screen.fill(Rect::new(area.x, y, area.w, 1), row_style);
448 }
449 let marker = if chosen { "" } else { " " };
450 self.screen.text(area.x, y, area.w, marker, row_style);
451 let cell = Rect::new(area.x + 2, y, area.w.saturating_sub(2), 1);
452 self.node(*item, cell, row_style, enabled);
453 }
454 }
455
456 fn scroll(&mut self, id: u32, area: Rect, style: Style, enabled: bool) {
457 let props = self.tree.props(id);
458 // The content is painted at its full height into a screen of its own,
459 // then the visible window of it is copied across. Doing it this way
460 // means every widget inside a scroll paints exactly as it would
461 // outside one — nothing has to know it is being clipped.
462 let content_h = self
463 .tree
464 .children(id)
465 .iter()
466 .map(|c| layout::height_for_width(self.tree, *c, area.w))
467 .sum::<u16>()
468 .max(1);
469 let max_offset = content_h.saturating_sub(area.h);
470 let offset = props.cells("offset", 0).min(max_offset);
Scroll the list under the pointer, and from where it actually is a785201 nandi 16d ago471 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 ago472
Measure a node once, and paint only what is on screen c41903b nandi 15d ago473 // Only the part of the content the viewport is showing is painted.
474 // A backlog is a hundred messages and a screen holds a dozen; painting
475 // the whole column into a grid that tall and copying a window out of
476 // it costs the same on the ninetieth message nobody is looking at as
477 // on the one they are reading — which is what made scrolling a long
478 // conversation cost more than scrolling a short one.
479 //
480 // `band` is the rows worth painting: the visible window, grown to whole
481 // children at each end so that a message straddling an edge is laid out
482 // in one piece and cut by the copy rather than by the layout. Its top
483 // is where the buffer's row 0 is, and everything the pass below learned
484 // is in the buffer's coordinates — so it is moved back into the
485 // content's before the rest of this reads it against `offset`.
486 let full = Rect::new(0, 0, area.w, content_h);
487 let rects = layout::children_rects(self.tree, id, full);
488 let kids = self.tree.children(id);
489 let seen = offset..offset.saturating_add(area.h);
490 let mut base = seen.start;
491 let mut foot = seen.end.min(content_h);
492 // In order, and every child accounted for: the ones on screen are
493 // painted, and the ones that are not still take their place in the
494 // focus ring below.
495 let mut plan = Vec::with_capacity(kids.len());
496 for (child, rect) in kids.iter().zip(&rects) {
497 let shown = rect.y < seen.end && rect.y.saturating_add(rect.h) > seen.start;
498 if shown {
499 base = base.min(rect.y);
500 foot = foot.max(rect.y.saturating_add(rect.h));
501 }
502 plan.push((*child, *rect, shown));
503 }
504 let band = foot.saturating_sub(base).max(1);
505
506 let mut buffer = Screen::new(area.w, band);
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago507 let mut inner = Painter {
508 tree: self.tree,
509 screen: &mut buffer,
510 focus: self.focus,
511 caret: self.caret,
512 tick: self.tick,
513 out: Painted::default(),
514 overlays: Vec::new(),
515 };
Measure a node once, and paint only what is on screen c41903b nandi 15d ago516 for (child, rect, shown) in plan {
517 if !shown {
518 inner.ring_only(child, enabled);
519 continue;
520 }
521 // The same clip `children` applies, against the content rather than
522 // the band: a child asking for more than the column has paints what
523 // fits. Nothing is clipped to the band itself — a child hanging off
524 // either end of it is what the copy below is for.
525 let width = rect.w.min(full.w.saturating_sub(rect.x));
526 inner.node(
527 child,
528 Rect::new(rect.x, rect.y - base, width, rect.h),
529 style,
530 enabled,
531 );
532 }
533 let mut learned = inner.out;
534 learned.shift_down(base);
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago535
536 for y in 0..area.h {
537 for x in 0..area.w {
Measure a node once, and paint only what is on screen c41903b nandi 15d ago538 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 ago539 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 ago540 }
541 }
542 }
543 // Widgets inside keep their place in the focus ring; their rects move
544 // by the viewport, and the ones scrolled out of sight take no clicks.
545 self.out.ring.extend(learned.ring);
546 for (node, rect) in learned.hits {
547 if rect.y >= offset && rect.y < offset.saturating_add(area.h) {
548 self.out.hits.push((
549 node,
550 Rect::new(
551 area.x + rect.x,
552 area.y + rect.y - offset,
553 rect.w,
554 rect.h.min(area.h),
555 ),
556 ));
557 }
558 }
Scroll the list under the pointer, and from where it actually is a785201 nandi 16d ago559 // A scroll inside this one was painted into the buffer, so its area is
560 // in the buffer's coordinates: move it the way the hits above moved,
561 // and drop the ones the viewport is not showing. A wheel over a nested
562 // list has to land on the list under the pointer, and a rect left in
563 // 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 ago564 // A picture inside a scroll moves with it, and is cut off by the
565 // viewport rather than painted over what is above or below: the
566 // protocol crops from the source, so a backlog scrolls past a picture
567 // a row at a time instead of losing it whole at the edge.
568 let bottom = offset.saturating_add(area.h);
569 for mut placement in learned.images {
570 let top = placement.area.y;
571 let foot = top.saturating_add(placement.area.h);
572 let seen_top = top.max(offset);
573 let seen_foot = foot.min(bottom);
574 if seen_foot <= seen_top {
575 continue;
576 }
577 placement.crop_top += seen_top - top;
578 placement.crop_bottom += foot - seen_foot;
579 placement.area = Rect::new(
580 area.x + placement.area.x,
581 area.y + seen_top - offset,
582 placement.area.w,
583 seen_foot - seen_top,
584 );
585 self.out.images.push(placement);
586 }
Scroll the list under the pointer, and from where it actually is a785201 nandi 16d ago587 for (node, inner_offset, max, rect) in learned.scrolled {
588 if rect.y >= offset && rect.y < offset.saturating_add(area.h) {
589 self.out.scrolled.push((
590 node,
591 inner_offset,
592 max,
593 Rect::new(
594 area.x + rect.x,
595 area.y + rect.y - offset,
596 rect.w,
597 rect.h.min(area.h),
598 ),
599 ));
600 }
601 }
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago602 if let Some((cx, cy)) = learned.cursor {
603 if cy >= offset && cy < offset.saturating_add(area.h) {
604 self.out.cursor = Some((area.x + cx, area.y + cy - offset));
605 }
606 }
607 }
608
609 fn overlay(&mut self, id: u32, screen: Rect) {
610 let props = self.tree.props(id);
611 let w = layout::width(self.tree, id, false).min(screen.w);
612 let h = layout::height_for_width(self.tree, id, w).min(screen.h);
613 let (x, y) = (
614 screen.x + Align::Center.offset_pub(w, screen.w),
615 screen.y + Align::Center.offset_pub(h, screen.h),
616 );
617 let area = Rect::new(x, y, w, h);
618 let style = self.style_for(&props, Style::default(), true);
619 // Blank what is under it: a floating panel that shows the screen
620 // through its gaps is unreadable.
621 for row in area.y..area.y + area.h {
622 for col in area.x..area.x + area.w {
623 self.screen.set(col, row, ' ', style);
624 }
625 }
626 self.border(area, props.label(), style);
627 let pad = layout::inset(&Tag::Overlay, &props);
628 self.children(id, area.shrink(pad), style, true);
629 }
630
631 /// A single-line box, with `label` set into the top edge when there is one.
632 fn border(&mut self, area: Rect, label: &str, style: Style) {
633 if area.w < 2 || area.h < 2 {
634 return;
635 }
636 let (x1, y1) = (area.x + area.w - 1, area.y + area.h - 1);
637 for x in area.x..=x1 {
638 self.screen.set(x, area.y, '─', style);
639 self.screen.set(x, y1, '─', style);
640 }
641 for y in area.y..=y1 {
642 self.screen.set(area.x, y, '│', style);
643 self.screen.set(x1, y, '│', style);
644 }
645 self.screen.set(area.x, area.y, '┌', style);
646 self.screen.set(x1, area.y, '┐', style);
647 self.screen.set(area.x, y1, '└', style);
648 self.screen.set(x1, y1, '┘', style);
649 if !label.is_empty() && area.w > 4 {
650 let text = format!(" {label} ");
651 self.screen.text(
652 area.x + 1,
653 area.y,
654 area.w - 2,
655 &text,
656 style.with(attr::BOLD),
657 );
658 }
659 }
660}
661
662impl Align {
663 /// [`Align::offset`] is private to the layout module; overlays are the one
664 /// caller outside it that centres something by hand.
665 fn offset_pub(self, size: u16, avail: u16) -> u16 {
666 layout::place(self, size, avail).0
667 }
668}