nandi/jolt-nativepublic Fork 0
121e5f1d751e8003ea229e70debf486b9bea3286
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 · 717 lines · 28.6 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
Give the terminal's entry a caret that means what it says 361b4dc nandi 9d ago12use crate::entry::View;
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago13use crate::graphics;
Run the formatter over the tree 3e8c6f0 nandi 13d ago14use crate::layout::{self, wrap, Align};
Make the entry's caret a cell rather than the whole field 4e714e5 nandi 9d ago15use crate::screen::{attr, Cell, Color, Rect, Screen, Style};
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago16use crate::tree::{Props, Tag, Tree};
17
18const SPINNER: [char; 8] = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧'];
19
20/// What one frame of painting learned about the tree, for the input half to
21/// use on the next key or click.
22#[derive(Clone, Debug, Default)]
23pub struct Painted {
24 /// Focusable nodes in paint order — the order Tab walks.
25 pub ring: Vec<u32>,
26 /// Where each of them ended up.
27 pub hits: Vec<(u32, Rect)>,
28 /// How far each scroll node's viewport actually was, after clamping to the
29 /// 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 ago30 /// 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 ago31 /// furthest it could have been, and the area it was painted into). The
32 /// second number is what tells a caller whether it is at the bottom, which
33 /// is what sticking to it means; the rect is what a wheel is aimed at.
34 pub scrolled: Vec<(u32, u16, u16, Rect)>,
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago35 /// Where the cursor should sit — the focused entry's caret, if any.
36 pub cursor: Option<(u16, u16)>,
Give the terminal's entry a caret that means what it says 361b4dc nandi 9d ago37 /// How far down the focused entry was scrolled, in rows of its own text.
38 /// A tall draft in a short field keeps its place between frames, and the
39 /// input half needs the same number to turn a click into a character.
40 pub entry_top: usize,
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago41 /// The pictures this frame wants on screen, in the cells they were given.
42 /// Nothing was painted for them: the grid has no pixels, and the terminal
43 /// is what draws one — see [`crate::graphics`].
44 pub images: Vec<graphics::Placement>,
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago45}
46
Measure a node once, and paint only what is on screen c41903b nandi 16d ago47impl Painted {
48 /// Move everything down by `rows`.
49 ///
50 /// A scroll paints its content into a buffer that starts partway down the
51 /// column, so what came back is in that buffer's coordinates. This puts it
52 /// back into the content's, where the viewport's own offset means what it
53 /// says.
54 fn shift_down(&mut self, rows: u16) {
55 if rows == 0 {
56 return;
57 }
58 for (_, rect) in &mut self.hits {
59 rect.y = rect.y.saturating_add(rows);
60 }
61 for (_, _, _, rect) in &mut self.scrolled {
62 rect.y = rect.y.saturating_add(rows);
63 }
64 for placement in &mut self.images {
65 placement.area.y = placement.area.y.saturating_add(rows);
66 }
67 if let Some((_, y)) = &mut self.cursor {
68 *y = y.saturating_add(rows);
69 }
70 }
71}
72
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago73struct Painter<'a> {
74 tree: &'a Tree,
75 screen: &'a mut Screen,
76 focus: u32,
77 /// Where the caret sits in the focused entry's text, in characters.
78 caret: usize,
Give the terminal's entry a caret that means what it says 361b4dc nandi 9d ago79 /// The row the focused entry was scrolled to on the last frame.
80 entry_top: usize,
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago81 tick: u64,
82 out: Painted,
83 overlays: Vec<u32>,
84}
85
86/// Paint the whole tree. `focus` is the node the ring is currently on and
87/// `tick` advances the spinners.
Give the terminal's entry a caret that means what it says 361b4dc nandi 9d ago88pub fn frame(
89 tree: &Tree,
90 screen: &mut Screen,
91 focus: u32,
92 caret: usize,
93 entry_top: usize,
94 tick: u64,
95) -> Painted {
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago96 screen.clear();
97 let mut painter = Painter {
98 tree,
99 screen,
100 focus,
101 caret,
Give the terminal's entry a caret that means what it says 361b4dc nandi 9d ago102 entry_top,
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago103 tick,
Give the terminal's entry a caret that means what it says 361b4dc nandi 9d ago104 out: Painted {
105 entry_top,
106 ..Painted::default()
107 },
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago108 overlays: Vec::new(),
109 };
110 let area = painter.screen.rect();
111 painter.node(tree.root(), area, Style::default(), true);
112
113 // Overlays float above the rest, so they are painted after it — and a
114 // click landing on one must beat a click on whatever it covers, which is
115 // what putting their hit rects first does.
116 let overlays = std::mem::take(&mut painter.overlays);
117 let below = std::mem::take(&mut painter.out.hits);
118 for id in overlays {
119 painter.overlay(id, area);
120 }
121 painter.out.hits.extend(below);
122 painter.out
123}
124
125impl Painter<'_> {
126 fn style_for(&self, props: &Props, inherited: Style, enabled: bool) -> Style {
127 let mut style = inherited;
128 if let Some(fg) = Color::parse(props.str("color")) {
129 style.fg = fg;
130 }
131 if let Some(bg) = Color::parse(props.str("bg")) {
132 style.bg = bg;
133 }
134 for (key, bit) in [
135 ("bold", attr::BOLD),
136 ("dim", attr::DIM),
137 ("underline", attr::UNDERLINE),
138 ("reverse", attr::REVERSE),
139 ("blink", attr::BLINK),
140 ("italic", attr::ITALIC),
141 ] {
142 if props.bool(key, false) {
143 style.attrs |= bit;
144 }
145 }
146 if !enabled {
147 // `:sensitive false` dims the widget *and its whole subtree*, which
148 // is what it means in every other glimmer backend.
149 style.attrs |= attr::DIM;
150 }
151 style
152 }
153
154 fn node(&mut self, id: u32, area: Rect, inherited: Style, enabled: bool) {
155 if area.is_empty() || !self.tree.exists(id) {
156 return;
157 }
158 let tag = self.tree.tag(id);
159 let props = self.tree.props(id);
160 let enabled = enabled && props.bool("sensitive", true);
161 let style = self.style_for(&props, inherited, enabled);
162 if props.has("bg") {
163 self.screen.fill(area, style);
164 }
165 if enabled && tag.focusable() {
166 self.out.ring.push(id);
167 self.out.hits.push((id, area));
168 }
169
170 let pad = layout::inset(&tag, &props);
171 let inner = area.shrink(pad);
172 match tag {
173 Tag::Overlay => self.overlays.push(id),
174 Tag::Frame => {
175 self.border(area, props.label(), style);
176 self.children(id, inner, style, enabled);
177 }
178 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 ago179 Tag::Box | Tag::Window => self.children(id, inner, style, enabled),
180 // A tag this backend has not learned paints as a vertical box, so
181 // whatever is under it still reaches the screen. When there is
182 // nothing under it, its own text does instead: an unknown *leaf*
183 // is a widget the caller has and this has not — frq's `:status`
184 // badge, its `:link` — and painting the box and not the label is
185 // the one outcome that loses the text altogether. A link vanishing
186 // out of the middle of a message is not a missing widget; it is a
187 // missing sentence.
188 Tag::Unknown(_) => {
Let a backlog keep its place, and leave room for what is under it 68910bd nandi 17d ago189 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 ago190 self.wrapped(inner, props.label(), style);
191 } else {
192 self.children(id, inner, style, enabled);
193 }
194 }
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago195 Tag::Label => self.wrapped(inner, props.label(), style),
196 Tag::Title => self.wrapped(inner, props.label(), style.with(attr::BOLD)),
197 Tag::DimLabel => self.wrapped(inner, props.label(), style.with(attr::DIM)),
198 Tag::Button => self.button(id, inner, &props, style),
199 Tag::CheckButton => self.check(id, inner, &props, style),
200 Tag::Entry => self.entry(id, inner, &props, style),
201 Tag::Separator => self.separator(inner, style),
202 Tag::Progress => self.progress(inner, &props, style),
203 Tag::Spinner => {
204 let ch = SPINNER[(self.tick as usize) % SPINNER.len()];
205 self.screen.set(inner.x, inner.y, ch, style);
206 }
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago207 Tag::Image => self.image(id, inner, &props, style),
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago208 Tag::Reaction => self.reaction(id, inner, &props, style),
209 // The same glyph with nothing around it: a character in a line,
210 // and the line is what says anything about it.
211 Tag::Emoji => {
212 self.screen
213 .text(inner.x, inner.y, inner.w, props.str("emoji"), style);
214 }
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago215 Tag::Listbox => self.listbox(id, inner, &props, style, enabled),
216 // A spacer is the absence of anything; the clear at the top of the
217 // frame has already drawn it.
218 Tag::Spacer => {}
219 }
220 }
221
Measure a node once, and paint only what is on screen c41903b nandi 16d ago222 /// Put a subtree's focusable nodes into the ring without painting it.
223 ///
224 /// What a scroll owes the parts of its content it did not paint. Tab walks
225 /// the ring, and a reader tabbing onto a button below the fold is how they
226 /// scroll to it — so a widget being out of sight cannot take it out of the
227 /// order. It has no rect, which is exactly right: there is nowhere on the
228 /// screen to click something that is not on the screen.
229 fn ring_only(&mut self, id: u32, enabled: bool) {
230 if !self.tree.exists(id) {
231 return;
232 }
233 let enabled = enabled && self.tree.props_of(id).bool("sensitive", true);
234 if enabled && self.tree.tag_of(id).focusable() {
235 self.out.ring.push(id);
236 }
237 for child in self.tree.children_of(id) {
238 self.ring_only(*child, enabled);
239 }
240 }
241
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago242 fn children(&mut self, id: u32, area: Rect, style: Style, enabled: bool) {
243 if area.is_empty() {
244 return;
245 }
246 let rects = layout::children_rects(self.tree, id, area);
247 for (child, rect) in self.tree.children(id).into_iter().zip(rects) {
248 // Clip to the parent: a child asking for more rows than are left
249 // paints what fits rather than over its neighbours.
250 let bottom = area.y.saturating_add(area.h);
251 let right = area.x.saturating_add(area.w);
252 if rect.y >= bottom || rect.x >= right {
253 continue;
254 }
255 let clipped = Rect::new(
256 rect.x,
257 rect.y,
258 rect.w.min(right - rect.x),
259 rect.h.min(bottom - rect.y),
260 );
261 self.node(child, clipped, style, enabled);
262 }
263 }
264
265 fn wrapped(&mut self, area: Rect, text: &str, style: Style) {
266 for (i, line) in wrap(text, area.w).into_iter().enumerate() {
267 if i as u16 >= area.h {
268 break;
269 }
270 self.screen
271 .text(area.x, area.y + i as u16, area.w, &line, style);
272 }
273 }
274
275 fn focused(&self, id: u32) -> bool {
276 self.focus == id
277 }
278
279 fn button(&mut self, id: u32, area: Rect, props: &Props, style: Style) {
280 let mut style = match props.str("kind") {
281 "primary" => style.with(attr::BOLD),
282 "destructive" => style.fg(Color::parse("red").unwrap_or_default()),
283 _ => style,
284 };
285 if self.focused(id) {
286 style = style.with(attr::REVERSE);
287 }
288 let label = format!("[ {} ]", props.label());
289 self.screen.text(area.x, area.y, area.w, &label, style);
290 }
291
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago292 /// A picture: the cells it was given, and a note of where they are.
293 ///
294 /// Nothing goes in them. The terminal draws the picture over the blank
295 /// cells when the frame is flushed, which is the only way pixels reach a
296 /// grid; where there is no protocol for that, the cells carry the note
297 /// that says a picture is here, and the link above it is the way to it.
298 fn image(&mut self, id: u32, area: Rect, props: &Props, style: Style) {
299 let path = props.str("src");
300 if path.is_empty() || area.is_empty() {
301 return;
302 }
303 if !graphics::supported() {
Run the formatter over the tree 3e8c6f0 nandi 13d ago304 self.screen.text(
305 area.x,
306 area.y,
307 area.w,
308 layout::PICTURE,
309 style.with(attr::DIM),
310 );
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago311 return;
312 }
313 // The column hands a child its whole width; a picture takes only what
314 // its shape asks for out of that, so the placement is the picture and
315 // not the room around it.
316 let (cols, rows) = layout::image_cells(props, area.w);
317 let area = Rect::new(area.x, area.y, cols.min(area.w), rows.min(area.h));
318 if area.is_empty() {
319 return;
320 }
321 self.out.images.push(graphics::Placement {
322 node: id,
323 path: path.to_owned(),
324 area,
325 crop_top: 0,
326 crop_bottom: 0,
327 });
328 }
329
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago330 /// A reaction pill: the glyph, the tally where there is one, and whether
331 /// you are on it.
332 ///
333 /// No border around it. A window draws a lozenge because it has half-cells
334 /// to draw one in; here brackets would cost two columns of a row that
335 /// already carries three chips, and would say "button" about a thing whose
336 /// whole picture is the glyph. Yours is bold, which is the one bit of the
337 /// pill a reader actually reads off it.
338 fn reaction(&mut self, id: u32, area: Rect, props: &Props, style: Style) {
339 let mut style = if props.bool("mine", false) {
340 style.with(attr::BOLD)
341 } else {
342 style
343 };
344 if self.focused(id) {
345 style = style.with(attr::REVERSE);
346 }
347 self.screen
348 .text(area.x, area.y, area.w, &layout::pill_text(props), style);
349 }
350
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago351 fn check(&mut self, id: u32, area: Rect, props: &Props, style: Style) {
352 let style = if self.focused(id) {
353 style.with(attr::REVERSE)
354 } else {
355 style
356 };
357 let mark = if props.bool("active", false) {
358 'x'
359 } else {
360 ' '
361 };
362 let label = format!("[{mark}] {}", props.label());
363 self.screen.text(area.x, area.y, area.w, &label, style);
364 }
365
366 fn entry(&mut self, id: u32, area: Rect, props: &Props, style: Style) {
367 let focused = self.focused(id);
368 let text = props.str("text");
369 let showing_placeholder = text.is_empty();
370 let shown = layout::entry_text(props);
371 let mut style = style.with(attr::UNDERLINE);
372 if showing_placeholder {
373 style = style.with(attr::DIM);
374 }
375 if focused {
Make the entry's caret a cell rather than the whole field 4e714e5 nandi 9d ago376 // Bold, not reverse. Reversing the whole rect painted the field as
377 // one lit block — which reads as a selection over every character
378 // in it, and hid the one cell that is actually the caret. The
379 // caret is a cell of its own below; this is only which field has
380 // the keyboard.
381 style = style.with(attr::BOLD);
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago382 }
383 // The field is its whole rect, not just the text in it: a reader needs
384 // to see where it can type before it has typed anything.
385 self.screen.fill(area, style);
Give the terminal's entry a caret that means what it says 361b4dc nandi 9d ago386 // Where the field is scrolled to, and where the caret sits in it, are
387 // one question with one answer — see [`crate::entry`]. The caret only
388 // belongs to the focused field; an unfocused one shows the end of what
389 // is in it, which is what was last typed there.
390 let caret = if focused {
391 self.caret.min(shown.chars().count())
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago392 } else {
Give the terminal's entry a caret that means what it says 361b4dc nandi 9d ago393 shown.chars().count()
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago394 };
Give the terminal's entry a caret that means what it says 361b4dc nandi 9d ago395 let was_top = if focused { self.entry_top } else { usize::MAX };
396 let view = View::of(
397 &shown,
398 area.w,
399 area.h,
400 layout::entry_multiline(props),
401 caret,
402 was_top,
403 );
404 for row in 0..view.rows.min(view.lines.len().saturating_sub(view.top)) {
405 self.screen.text(
406 area.x,
407 area.y + row as u16,
408 area.w,
409 &view.painted(view.top + row),
410 style,
411 );
412 }
413 if focused {
414 self.out.entry_top = view.top;
415 // In columns rather than characters: an emoji typed into the line
416 // is two cells wide, and a caret counted in characters sits a
417 // column left of the text for each one.
418 let (row, col) = if showing_placeholder {
419 (view.top, 0)
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago420 } else {
Give the terminal's entry a caret that means what it says 361b4dc nandi 9d ago421 view.caret_at(caret)
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago422 };
Give the terminal's entry a caret that means what it says 361b4dc nandi 9d ago423 let col = col.min(area.w.saturating_sub(1));
424 let row = row
425 .saturating_sub(view.top)
426 .min(area.h.saturating_sub(1) as usize);
Make the entry's caret a cell rather than the whole field 4e714e5 nandi 9d ago427 let (cx, cy) = (area.x.saturating_add(col), area.y + row as u16);
428 // The caret as a cell, as well as where the terminal is told to
429 // put its own cursor. The two are the same place; this is the one
430 // that survives a terminal that hides the cursor, a screenshot,
431 // and a test.
432 if let Some(under) = self.screen.cell(cx, cy).cloned() {
433 let style = under.style.with(attr::REVERSE);
434 self.screen.put(cx, cy, Cell { style, ..under });
435 }
436 self.out.cursor = Some((cx, cy));
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago437 }
438 }
439
440 fn separator(&mut self, area: Rect, style: Style) {
441 for x in area.x..area.x.saturating_add(area.w) {
442 self.screen.set(x, area.y, '─', style);
443 }
444 }
445
446 fn progress(&mut self, area: Rect, props: &Props, style: Style) {
447 let value = props.num("value", 0.0).clamp(0.0, 1.0);
448 let filled = (value * area.w as f64).round() as u16;
449 for x in 0..area.w {
450 let ch = if x < filled { '█' } else { '░' };
451 self.screen.set(area.x + x, area.y, ch, style);
452 }
453 let label = props.label();
454 if !label.is_empty() {
455 let at = area.x + (area.w.saturating_sub(label.chars().count() as u16)) / 2;
456 self.screen.text(at, area.y, area.w, label, style);
457 }
458 }
459
460 fn listbox(&mut self, id: u32, area: Rect, props: &Props, style: Style, enabled: bool) {
461 let items = self.tree.children(id);
462 // No `:selected` at all means the cursor is on the first row: a list
463 // with no cursor cannot be moved with the arrows, and a caller that
464 // wants none says so with -1.
465 let selected = props.num("selected", 0.0);
466 let selected = if selected < 0.0 {
467 None
468 } else {
469 Some(selected as usize)
470 };
471 // Keep the cursor on screen: scroll only as far as it takes.
472 let rows = area.h as usize;
473 let first = match selected {
474 Some(sel) if rows > 0 && sel >= rows => sel + 1 - rows,
475 _ => 0,
476 };
477 for (row, item) in items.iter().skip(first).take(rows).enumerate() {
478 let y = area.y + row as u16;
479 let chosen = selected == Some(first + row);
480 let mut row_style = style;
481 if chosen {
482 row_style = row_style.with(if self.focused(id) {
483 attr::REVERSE
484 } else {
485 attr::BOLD
486 });
487 self.screen.fill(Rect::new(area.x, y, area.w, 1), row_style);
488 }
489 let marker = if chosen { "" } else { " " };
490 self.screen.text(area.x, y, area.w, marker, row_style);
491 let cell = Rect::new(area.x + 2, y, area.w.saturating_sub(2), 1);
492 self.node(*item, cell, row_style, enabled);
493 }
494 }
495
496 fn scroll(&mut self, id: u32, area: Rect, style: Style, enabled: bool) {
497 let props = self.tree.props(id);
498 // The content is painted at its full height into a screen of its own,
499 // then the visible window of it is copied across. Doing it this way
500 // means every widget inside a scroll paints exactly as it would
501 // outside one — nothing has to know it is being clipped.
502 let content_h = self
503 .tree
504 .children(id)
505 .iter()
506 .map(|c| layout::height_for_width(self.tree, *c, area.w))
507 .sum::<u16>()
508 .max(1);
509 let max_offset = content_h.saturating_sub(area.h);
510 let offset = props.cells("offset", 0).min(max_offset);
Scroll the list under the pointer, and from where it actually is a785201 nandi 16d ago511 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 ago512
Measure a node once, and paint only what is on screen c41903b nandi 16d ago513 // Only the part of the content the viewport is showing is painted.
514 // A backlog is a hundred messages and a screen holds a dozen; painting
515 // the whole column into a grid that tall and copying a window out of
516 // it costs the same on the ninetieth message nobody is looking at as
517 // on the one they are reading — which is what made scrolling a long
518 // conversation cost more than scrolling a short one.
519 //
520 // `band` is the rows worth painting: the visible window, grown to whole
521 // children at each end so that a message straddling an edge is laid out
522 // in one piece and cut by the copy rather than by the layout. Its top
523 // is where the buffer's row 0 is, and everything the pass below learned
524 // is in the buffer's coordinates — so it is moved back into the
525 // content's before the rest of this reads it against `offset`.
526 let full = Rect::new(0, 0, area.w, content_h);
527 let rects = layout::children_rects(self.tree, id, full);
528 let kids = self.tree.children(id);
529 let seen = offset..offset.saturating_add(area.h);
530 let mut base = seen.start;
531 let mut foot = seen.end.min(content_h);
532 // In order, and every child accounted for: the ones on screen are
533 // painted, and the ones that are not still take their place in the
534 // focus ring below.
535 let mut plan = Vec::with_capacity(kids.len());
536 for (child, rect) in kids.iter().zip(&rects) {
537 let shown = rect.y < seen.end && rect.y.saturating_add(rect.h) > seen.start;
538 if shown {
539 base = base.min(rect.y);
540 foot = foot.max(rect.y.saturating_add(rect.h));
541 }
542 plan.push((*child, *rect, shown));
543 }
544 let band = foot.saturating_sub(base).max(1);
545
546 let mut buffer = Screen::new(area.w, band);
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago547 let mut inner = Painter {
548 tree: self.tree,
549 screen: &mut buffer,
550 focus: self.focus,
551 caret: self.caret,
Give the terminal's entry a caret that means what it says 361b4dc nandi 9d ago552 entry_top: self.entry_top,
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago553 tick: self.tick,
Give the terminal's entry a caret that means what it says 361b4dc nandi 9d ago554 out: Painted {
555 entry_top: self.entry_top,
556 ..Painted::default()
557 },
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago558 overlays: Vec::new(),
559 };
Measure a node once, and paint only what is on screen c41903b nandi 16d ago560 for (child, rect, shown) in plan {
561 if !shown {
562 inner.ring_only(child, enabled);
563 continue;
564 }
565 // The same clip `children` applies, against the content rather than
566 // the band: a child asking for more than the column has paints what
567 // fits. Nothing is clipped to the band itself — a child hanging off
568 // either end of it is what the copy below is for.
569 let width = rect.w.min(full.w.saturating_sub(rect.x));
570 inner.node(
571 child,
572 Rect::new(rect.x, rect.y - base, width, rect.h),
573 style,
574 enabled,
575 );
576 }
577 let mut learned = inner.out;
578 learned.shift_down(base);
Give the terminal's entry a caret that means what it says 361b4dc nandi 9d ago579 // A field inside a scroll is still the focused field, and what it
580 // learned about its own scroll has to come back out with it.
581 if learned.entry_top != self.entry_top {
582 self.out.entry_top = learned.entry_top;
583 }
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago584
585 for y in 0..area.h {
586 for x in 0..area.w {
Measure a node once, and paint only what is on screen c41903b nandi 16d ago587 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 ago588 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 ago589 }
590 }
591 }
592 // Widgets inside keep their place in the focus ring; their rects move
593 // by the viewport, and the ones scrolled out of sight take no clicks.
594 self.out.ring.extend(learned.ring);
595 for (node, rect) in learned.hits {
596 if rect.y >= offset && rect.y < offset.saturating_add(area.h) {
597 self.out.hits.push((
598 node,
599 Rect::new(
600 area.x + rect.x,
601 area.y + rect.y - offset,
602 rect.w,
603 rect.h.min(area.h),
604 ),
605 ));
606 }
607 }
Scroll the list under the pointer, and from where it actually is a785201 nandi 16d ago608 // A scroll inside this one was painted into the buffer, so its area is
609 // in the buffer's coordinates: move it the way the hits above moved,
610 // and drop the ones the viewport is not showing. A wheel over a nested
611 // list has to land on the list under the pointer, and a rect left in
612 // 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 ago613 // A picture inside a scroll moves with it, and is cut off by the
614 // viewport rather than painted over what is above or below: the
615 // protocol crops from the source, so a backlog scrolls past a picture
616 // a row at a time instead of losing it whole at the edge.
617 let bottom = offset.saturating_add(area.h);
618 for mut placement in learned.images {
619 let top = placement.area.y;
620 let foot = top.saturating_add(placement.area.h);
621 let seen_top = top.max(offset);
622 let seen_foot = foot.min(bottom);
623 if seen_foot <= seen_top {
624 continue;
625 }
626 placement.crop_top += seen_top - top;
627 placement.crop_bottom += foot - seen_foot;
628 placement.area = Rect::new(
629 area.x + placement.area.x,
630 area.y + seen_top - offset,
631 placement.area.w,
632 seen_foot - seen_top,
633 );
634 self.out.images.push(placement);
635 }
Scroll the list under the pointer, and from where it actually is a785201 nandi 16d ago636 for (node, inner_offset, max, rect) in learned.scrolled {
637 if rect.y >= offset && rect.y < offset.saturating_add(area.h) {
638 self.out.scrolled.push((
639 node,
640 inner_offset,
641 max,
642 Rect::new(
643 area.x + rect.x,
644 area.y + rect.y - offset,
645 rect.w,
646 rect.h.min(area.h),
647 ),
648 ));
649 }
650 }
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago651 if let Some((cx, cy)) = learned.cursor {
652 if cy >= offset && cy < offset.saturating_add(area.h) {
653 self.out.cursor = Some((area.x + cx, area.y + cy - offset));
654 }
655 }
656 }
657
658 fn overlay(&mut self, id: u32, screen: Rect) {
659 let props = self.tree.props(id);
660 let w = layout::width(self.tree, id, false).min(screen.w);
661 let h = layout::height_for_width(self.tree, id, w).min(screen.h);
662 let (x, y) = (
663 screen.x + Align::Center.offset_pub(w, screen.w),
664 screen.y + Align::Center.offset_pub(h, screen.h),
665 );
666 let area = Rect::new(x, y, w, h);
667 let style = self.style_for(&props, Style::default(), true);
668 // Blank what is under it: a floating panel that shows the screen
669 // through its gaps is unreadable.
670 for row in area.y..area.y + area.h {
671 for col in area.x..area.x + area.w {
672 self.screen.set(col, row, ' ', style);
673 }
674 }
675 self.border(area, props.label(), style);
676 let pad = layout::inset(&Tag::Overlay, &props);
677 self.children(id, area.shrink(pad), style, true);
678 }
679
680 /// A single-line box, with `label` set into the top edge when there is one.
681 fn border(&mut self, area: Rect, label: &str, style: Style) {
682 if area.w < 2 || area.h < 2 {
683 return;
684 }
685 let (x1, y1) = (area.x + area.w - 1, area.y + area.h - 1);
686 for x in area.x..=x1 {
687 self.screen.set(x, area.y, '─', style);
688 self.screen.set(x, y1, '─', style);
689 }
690 for y in area.y..=y1 {
691 self.screen.set(area.x, y, '│', style);
692 self.screen.set(x1, y, '│', style);
693 }
694 self.screen.set(area.x, area.y, '┌', style);
695 self.screen.set(x1, area.y, '┐', style);
696 self.screen.set(area.x, y1, '└', style);
697 self.screen.set(x1, y1, '┘', style);
698 if !label.is_empty() && area.w > 4 {
699 let text = format!(" {label} ");
700 self.screen.text(
701 area.x + 1,
702 area.y,
703 area.w - 2,
704 &text,
705 style.with(attr::BOLD),
706 );
707 }
708 }
709}
710
711impl Align {
712 /// [`Align::offset`] is private to the layout module; overlays are the one
713 /// caller outside it that centres something by hand.
714 fn offset_pub(self, size: u16, avail: u16) -> u16 {
715 layout::place(self, size, avail).0
716 }
717}