//! Regions into rects. //! //! The half of the drawing a webview never has to do. A stylesheet turns //! `list-detail` into two columns and the browser does the arithmetic; here the //! arithmetic is the renderer's, and every place the description does not say //! enough to do it is a finding. //! //! Three of them were recorded on `179b088d`, and **all three have since been //! answered by the vocabulary rather than worked around here.** Kept as a //! record of what a description had to grow, since each was found by trying to //! draw a screen in a terminal and finding nothing to draw it from: //! //! - **A tabbed arrangement did not say which tab is showing**, so this drew //! the first and called it a guess. [`layout::Showing`] says it now, and //! [`Slot::current`] reads it; see [`showing_body`]. //! - **A tab had no label**, so a heading here would have been [`Slot::id`], //! an address chosen for fragment targeting. [`Slot::label`] says it now, //! gathered by [`Slot::labels`]. //! - **Nothing said a region's share**, so a sidebar was 24 columns and a list //! pane 40% because this renderer picked those numbers. `Arrangement::share` //! says it now, `e0fd485e`. use makeover_layout as layout; use makeover_tui::{frame, text}; use quasi_router::{Node, Ranked, RegionKind, Run, Screen, Slot}; use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::style::{Modifier, Style}; use ratatui::text::Span; use crate::{Local, Pass, Tui, below}; // The sidebar's 24 columns and the list pane's 40% used to be declared here, // as two numbers this renderer chose with nothing behind them. `e0fd485e`: // they are `Arrangement::share` now, so the terminal and the webview honour one // fact and two hosts showing one screen agree about its proportions. /// The cutoff a region narrows to at this width, in cells. /// /// The terminal's answer to the question `@media` answers in a browser and /// `makeover-geometry`'s [`SizeClass`] boundaries answer in points. The three /// tiers are the same three; the numbers cannot be, because a size class is /// quoted in CSS pixels and a terminal has cells. Material's 600 and 840 /// divided by a nominal 10px cell is where these come from, so a terminal /// window and a browser window showing one screen narrow at roughly the same /// physical width rather than at unrelated points. /// /// A guess in the same sense `makeover-tui`'s table sizing fallback is one: the /// description carries no magnitude and one has to be supplied here or the /// member cannot be honoured at all. What is *not* a guess is the order, which /// is [`quasi_router::CUTOFFS`], and the fact that nothing counts -- the cutoff /// comes off this width alone, so the same width is the same answer whatever /// widths came before it. /// /// [`SizeClass`]: https://docs.rs/makeover-geometry fn cutoff(width: u16) -> layout::Priority { match width { 0..60 => layout::Priority::Essential, 60..84 => layout::Priority::Secondary, _ => layout::Priority::Optional, } } /// The members a region shows at this width. fn kept<'a>(body: &[&'a Ranked], width: u16) -> impl Iterator { let cutoff = cutoff(width); body.iter() .copied() .filter(move |placed| placed.kept_at(cutoff)) .map(|placed| &placed.node) } /// Lay a screen's regions out and draw them. pub(crate) fn screen_regions(pass: &mut Pass<'_>, screen: &Screen, area: Rect, buf: &mut Buffer) { let area = measured(screen.measure, area); let mut rest = area; // Bands take the rows they need and get out of the way; which way they get // out of is where they were said. Ruled by Max 2026-08-23 (quasicoherent // `3725bacf`), picking (b) of two: a band before the body sits above it and // a band after it sits below, so a footer is a band at the end rather than // a member the vocabulary had to grow. quasi-webview has emitted slots in // declaration order all along; this renderer and quasi-immediate hoisted, // so the ruling settled a disagreement rather than a silence. let (leading, trailing) = bands(screen); for slot in &leading { let used = draw(pass, slot, rest, buf); rest = below(rest, used); } // The bottom is reserved before the body is laid out, or a pane that fills // its area would leave the footer nowhere to go. Reserved by asking each // trailing band how tall it is, which this renderer can do exactly -- // `height` is the same arithmetic the scroll already trusts -- so nothing // here is a guess and no number is authored. let reserved = trailing .iter() .map(|slot| height(pass.tui, slot, rest.width, &pass.local())) .sum::() .min(rest.height); let mut feet = below(rest, rest.height - reserved); rest = Rect { height: rest.height - reserved, ..rest }; let body = body_slots(screen); match screen.arrangement { layout::Arrangement::SidebarContent { share } => { let (left, right) = split(rest, share.of(rest.width)); let mut sidebars = 0; let mut content = right; for slot in &body { if matches!(slot.kind, RegionKind::Sidebar) { let used = draw(pass, slot, below(left, sidebars), buf); sidebars += used; } else { let used = draw(pass, slot, content, buf); content = below(content, used); } } } layout::Arrangement::ListDetail { tabbed, share } => { if tabbed { // One at a time, and nothing says which. `body_slots` has // already cut the rest away, so this is the one region there is. if let Some(first) = body.first() { draw(pass, first, rest, buf); } } else { let (left, right) = split(rest, share.of(rest.width)); let mut detail = right; for (index, slot) in body.iter().enumerate() { if index == 0 { draw(pass, slot, left, buf); } else { let used = draw(pass, slot, detail, buf); detail = below(detail, used); } } } } // One region filling the width. Every body slot stacks down the whole // of `rest`, because there is no division to put anything beside. layout::Arrangement::Single => { let mut content = rest; for slot in &body { let used = draw(pass, slot, content, buf); content = below(content, used); } } } // The trailing bands, in the room kept for them, in the order they were // said: the band said last is the one at the bottom. for slot in &trailing { let used = draw(pass, slot, feet, buf); feet = below(feet, used); } // Modals last and over everything, which is what a modal is. Centred in // half the width, because `Depth::Overlay` says it sits above the page and // says nothing about how much of it to cover. for slot in screen .slots .iter() .filter(|slot| matches!(slot.kind, RegionKind::Modal)) { draw(pass, slot, centred(area), buf); } } /// The screen's area, narrowed to the measure it asked for. /// /// The terminal's half of "every renderer owes an answer". The description /// says how wide the content should run and this says what that is in columns, /// the same division the webview makes: the screen chose one of three, and /// what each one comes to is the renderer's. /// /// The two caps are this renderer's numbers, and only the second has a reason /// outside taste: past roughly 75 characters a line costs the reader the return /// sweep, which is why [`layout::Measure::Reading`] is the narrowest. Centred /// rather than left-aligned, because a narrowed column against the left edge of /// a wide terminal reads as a window that failed to resize. /// /// A terminal narrower than the cap is left alone rather than padded. There is /// no measure to enforce when the window is already tighter than it. fn measured(measure: layout::Measure, area: Rect) -> Rect { let cap = match measure { layout::Measure::Reading => 76, layout::Measure::Contained => 100, // Every column there is, which is what `Wide` means. Also the arm a // member added upstream lands in: a measure this renderer has not // learned should show the whole screen, not hide part of it. _ => return area, }; if area.width <= cap { return area; } Rect { x: area.x + (area.width - cap) / 2, width: cap, ..area } } /// The regions that fill the body, after the arrangement has had its say. /// /// The tabbed cut lives here and only here. It is the one place a described /// region can be on the screen or not, so the drawing and the focus walk have /// to agree about it, and two copies of "the first one, and nothing says which" /// is two chances to disagree. fn body_slots(screen: &Screen) -> Vec<&Slot> { let body = screen .slots .iter() .filter(|slot| !matches!(slot.kind, RegionKind::Band | RegionKind::Modal)); match screen.arrangement { layout::Arrangement::ListDetail { tabbed: true, .. } => body.take(1).collect(), _ => body.collect(), } } /// A screen's bands, as the ones above the body and the ones below it. /// /// The split is at the first region that is not a band or a modal. A screen of /// nothing but bands is all leading, which is the shape every description /// written before the ruling already had. fn bands(screen: &Screen) -> (Vec<&Slot>, Vec<&Slot>) { let is_band = |slot: &&Slot| matches!(slot.kind, RegionKind::Band); let first_body = screen .slots .iter() .position(|slot| !matches!(slot.kind, RegionKind::Band | RegionKind::Modal)); let Some(at) = first_body else { return (screen.slots.iter().filter(is_band).collect(), Vec::new()); }; ( screen.slots[..at].iter().filter(is_band).collect(), screen.slots[at..].iter().filter(is_band).collect(), ) } /// Every region the user can see, in the order it is drawn. /// /// What the focus walk reads. A region that is not drawn holds nothing /// reachable, which is why this is a question about slots rather than about /// nodes: a tab that is not showing has controls in it, and stopping on one /// would move focus to a place with nothing on screen. /// /// **A modal takes the whole of it.** A dialog you can tab out of is not a /// dialog, and this is the one place the drawing order and the focus order /// deliberately differ: the screen behind a modal is still painted, because /// covering it costs rows and says nothing, and it is still unreachable. pub(crate) fn reachable(screen: &Screen) -> Vec<&Slot> { let modals: Vec<&Slot> = screen .slots .iter() .filter(|slot| matches!(slot.kind, RegionKind::Modal)) .collect(); if !modals.is_empty() { return modals; } let (leading, trailing) = bands(screen); leading .into_iter() .chain(body_slots(screen)) .chain(trailing) .collect() } /// How a region's row packs at this width: the members on each line, and how /// tall the line is. /// /// Ruling: wiki `layout-room-and-fallback`. /// /// Packing is left to right, one space between members, wrapping to a new line /// when what is left of this one cannot hold the next member. Every width is /// derived -- [`crate::node::want`] measures a member from what it holds -- so /// nothing here is authored and there is no breakpoint. /// /// # What each fallback gets /// /// [`Wrap`](layout::Fallback::Wrap) and [`Stack`](layout::Fallback::Stack) keep /// every member and wrap. They differ in a webview by whether a wrapped member /// fills its line; a terminal has no such distinction to draw, so both answer /// alike rather than this renderer inventing one. /// /// [`Shed`](layout::Fallback::Shed) drops members by [`layout::Priority`] at /// the cutoff the region's body already reads, which is the half a webview /// cannot do at all. /// /// [`Menu`](layout::Fallback::Menu) **wraps rather than shedding**, and that is /// this renderer's answer rather than a shortfall. Menu says the shed members /// stay reachable behind one control; a terminal has no anchored menu to put /// them behind -- `quasi-tui`'s own header says a menu here is a key -- and a /// marker that shows a count nobody can open is the `by_host` failure, a thing /// drawn, reachable and doing nothing. Keeping every member on a second line /// honours the half that matters and states the half it cannot. fn packed<'a>(tui: &Tui, run: &'a Run, width: u16) -> Vec> { let kept: Vec<&Ranked> = match run.fallback { layout::Fallback::Shed => run.kept_at(cutoff(width)), _ => run.members.iter().collect(), }; let mut lines: Vec> = Vec::new(); let mut line: Vec<(&Ranked, u16)> = Vec::new(); let mut left = width; for placed in kept { let wants = match crate::node::want(tui, &placed.node) { crate::node::Want::Cells(cells) => cells.min(width), crate::node::Want::Rest => left.max(MEMBER_FLOOR).min(width), }; let gap = u16::from(!line.is_empty()); if !line.is_empty() && wants + gap > left { lines.push(std::mem::take(&mut line)); left = width; } let gap = u16::from(!line.is_empty()); let given = wants.min(left.saturating_sub(gap)); line.push((placed, given)); left = left.saturating_sub(given + gap); } if !line.is_empty() { lines.push(line); } lines.into_iter().map(|line| filled(line, width)).collect() } /// One packed line with whatever is left over handed to the members that asked /// for it. /// /// The description's half of a width, which a terminal can answer exactly: /// [`layout::Width::Fill`] members share what the content-sized ones did not /// take, equally, which is that member's own stated rule and the same answer a /// webview's `flex: 1 1 0` gives. /// /// Done after packing rather than during it, because how much is left over is /// not known until the line is known: the wrap point is decided by what each /// member wants from its contents, and a member that asked to fill has no /// opinion about where the line ends. So the line is composed the way it always /// was and only the leftover changes hands. /// /// [`layout::Width::Fixed`] takes nothing extra. A run carries no size, so /// there is no share to fix a member at, and content is what it drew before. fn filled(line: Vec<(&Ranked, u16)>, width: u16) -> Vec<(&Node, u16)> { let gaps = u16::try_from(line.len().saturating_sub(1)).unwrap_or(u16::MAX); let taken: u16 = line.iter().map(|(_, given)| *given).sum::() + gaps; let left = width.saturating_sub(taken); let fills = u16::try_from( line.iter() .filter(|(placed, _)| matches!(placed.width, layout::Width::Fill)) .count(), ) .unwrap_or(u16::MAX); if left == 0 || fills == 0 { return line .into_iter() .map(|(placed, given)| (&placed.node, given)) .collect(); } // The remainder goes to the leading fills, one cell each, because a // terminal cannot divide a cell and dropping it would leave the row short // of the width it was given. let share = left / fills; let mut over = left % fills; line.into_iter() .map(|(placed, given)| { if !matches!(placed.width, layout::Width::Fill) { return (&placed.node, given); } let extra = share + u16::from(over > 0); over = over.saturating_sub(1); (&placed.node, given + extra) }) .collect() } /// The narrowest a member is given before the row wraps instead. /// /// Only reached by a member that asked for what is left of the line and found /// almost nothing there. Below this a control is not readable, so it takes the /// next line whole rather than a sliver of this one. const MEMBER_FLOOR: u16 = 8; /// The rows a region's leading row takes at this width. fn run_height(tui: &Tui, slot: &Slot, width: u16, local: &Local<'_>) -> u16 { let Some(run) = slot.run.as_ref() else { return 0; }; packed(tui, run, width) .iter() .map(|line| { line.iter() .map(|(node, cells)| crate::node::height(tui, node, *cells, local)) .max() .unwrap_or(0) }) .sum() } /// Draw a region's leading row, and answer the rows it used. fn run_body(pass: &mut Pass<'_>, slot: &Slot, inner: Rect, buf: &mut Buffer) -> u16 { let Some(run) = slot.run.as_ref() else { return 0; }; let lines = packed(pass.tui, run, inner.width); let mut used = 0; for line in lines { let area = below(inner, used); if area.height == 0 { break; } let mut column = 0; let mut tall = 0; for (node, cells) in line { let cell = Rect { x: area.x + column, width: cells.min(area.width.saturating_sub(column)), ..area }; if cell.width == 0 { break; } tall = tall.max(crate::node::draw(pass, node, cell, buf)); // One space between members, which is the only separator a // terminal row needs and the same one a strip of labels uses. column += cell.width + 1; } used += tall.max(1); } used.min(inner.height) } /// The rows a region wants at `width`. pub(crate) fn height(tui: &Tui, slot: &Slot, width: u16, local: &Local<'_>) -> u16 { // A region that does not apply right now takes no rows: this renderer // leaves it out rather than dimming it, and the measurement and the drawing // have to say so together or a footer band is reserved room nothing paints // into. `079a011e`. if local.out(&slot.id) { return 0; } let inner = width.saturating_sub(2); // A region showing one child at a time is as tall as its tallest child plus // the row that moves between them. Tallest rather than current, because this // has no `View` and so cannot know which child is up -- an over-estimate, // which for the one caller (scroll arithmetic) errs toward letting a region // scroll slightly further than it needs to rather than cutting it off. let body: u16 = if slot.showing().selective() { // Every child, not only the ones kept at this width: a region showing // one child at a time can be moved onto any of them, so the tallest is // the height it has to be able to be. slot.body .iter() .map(|placed| crate::node::height(tui, &placed.node, inner, local)) .max() .unwrap_or(0) + 1 } else if slot.repeating.is_some() { // The slots, plus a caption row over each and the two controls. Counted // the way `repeating_body` draws it, because a measurement that // disagreed with the drawing is a footer with rows nothing paints into. slot.body .iter() .map(|placed| { 1 + crate::node::height(tui, &placed.node, inner, local) + u16::from(removes_of(&placed.node).is_some()) }) .sum::() + 1 } else { // At the same cutoff the drawing uses, or the scroll arithmetic and // the picture disagree about how much there is. kept(&slot.body.members().collect::>(), inner) .map(|node| crate::node::height(tui, node, inner, local)) .sum() }; // The host's rows under the described ones, and only for a bespoke region: // a fill named against a pane is a host reaching into a region the // description already owns, which is the rule the drawing keeps too. let filled = match (&slot.kind, tui.fill(&slot.id)) { (RegionKind::Handover { .. } | RegionKind::Ceded { .. }, Some(fill)) => { fill.rows(tui, inner) } // A handover with no fill says so and spends the rows to do it. A ceded // region says nothing, because nothing is owed. See `node::UNFILLED`. (RegionKind::Handover { .. }, None) => text::height(crate::node::UNFILLED, inner), _ => 0, }; // Two rows for the frame, when the region has one. run_height(tui, slot, inner, local) + body + filled + if framed(slot.kind.depth()) { 2 } else { 0 } } /// Draw one region, and answer the rows it used. pub(crate) fn draw(pass: &mut Pass<'_>, slot: &Slot, area: Rect, buf: &mut Buffer) -> u16 { // A region that does not apply right now is not on the screen at all, and // nothing in it is reachable. `079a011e`: the region names the control and // the value that bring it out, and this renderer answers it by leaving the // region out -- one of the three the ruling names, and the one a reader // does not have to skip past. // // No count is advanced with it, and the focus walk skips the same region // from the same list, so the caret and the drawing still agree about how // many stops there are. if pass.local().out(&slot.id) { return 0; } // A region still loading holds nothing reachable, here and in the focus // walk both: what is on the screen is the word "Loading", and a control // counted under it would be a place the caret could go with nothing to see. let pending = matches!(slot.readiness, layout::Readiness::Pending); if area.width == 0 || area.height == 0 { if !pending { for placed in slot .run .iter() .flat_map(|run| run.members.iter()) .chain(slot.body.iter()) { crate::node::draw(pass, &placed.node, area, buf); } } return 0; } let tui = pass.tui; // The frame, from the depth the region's kind implies. This is the whole // reason `makeover-tui` is a dependency rather than a nice-to-have: a // raised region is drawn the same way here as in every other terminal app // in the tree, bevel included, and the depth comes off the vocabulary // rather than off this renderer's taste. let depth = slot.kind.depth(); let inner = if framed(depth) { frame(buf, area, depth, tui.palette()) } else { area }; // `Readiness` is the loading axis, and a terminal has no spinner that is // not a clock. It says so in words instead, which loses the motion and // keeps the fact. if pending { let used = text::draw( "Loading", Style::default() .fg(tui.theme().content_muted) .add_modifier(Modifier::ITALIC), inner, buf, ); return used + if framed(depth) { 2 } else { 0 }; } // A bespoke region is the host's. The description named the place and the // blocks it owns above the fill, so those draw first; what goes under them // is the host's own drawing, handed the rows that are left. // // `Tui::with_fill` is the counterpart to `Webview::with_fill`, and until // `d86122cf` this renderer had none: a bespoke region drew its described // blocks and then stopped, whatever the host had to put in it. The // ordering is the arrangement `Containment::Opaque` describes -- a heading // the description owns above a canvas it does not. // The row the description said its members share, above the body. Above, // because that is the order the webview emits them in and the order a // toolbar over a list reads in. let row = run_body(pass, slot, inner, buf); let rest = below(inner, row); let mut used = row + if slot.showing().selective() { showing_body(pass, slot, rest, buf) } else if let Some(repeating) = slot.repeating.as_deref() { repeating_body(pass, slot, repeating, rest, buf) } else { body( pass, slot, &slot.body.members().collect::>(), rest, buf, ) }; if let RegionKind::Handover { .. } | RegionKind::Ceded { .. } = slot.kind { if let Some(fill) = tui.fill(&slot.id) { used = (used + fill.draw(tui, below(inner, used), buf)).min(inner.height); } else if slot.kind.as_layout().owed() { // The half the split exists for. Before it, a region the app had // ruled undescribable and one nobody had filled yet were the same // value here, and both drew as an empty box. let drawn = text::draw( crate::node::UNFILLED, tui.style().muted, below(inner, used), buf, ); used = (used + drawn).min(inner.height); } } // `Slot::id` is not drawn anywhere. It is a fragment address, and a // terminal redraws rather than swapping, so it costs nothing and says // nothing here. used + if framed(depth) { 2 } else { 0 } } /// What taking one slot away calls, when this node is a slot that can go. /// /// Read here rather than matched at each call site because the drawing, the /// measurement and the focus walk all ask it, and three spellings of "is this a /// region with a remove on it" is how three walks come to disagree. pub(crate) fn removes_of(node: &Node) -> Option<&quasi_router::Act> { match node { Node::Region(child) => child.removes.as_deref(), _ => None, } } /// One control, as the act it is, at whatever the floor or the ceiling says. /// /// The boundary is drawn rather than hidden: audiofiles' rule editor already /// made that call for its last condition, and a control that vanishes at a /// boundary is one the reader has to discover twice. What changed is that /// `Repeating` says it once instead of each app disabling its own button. pub(crate) fn bounded(act: &quasi_router::Act, allowed: bool) -> quasi_router::Act { if allowed { act.clone() } else { act.clone().disabled() } } /// A region whose children are answers to one question. /// /// Each slot under its number, with the control that takes it away, and the /// control that adds one under the lot. /// /// Everything is derived as nodes and drawn through [`crate::node::draw`], so /// this invents no styling: a slot's number is the same heading a described one /// gets, and the controls take everything `act_line` knows about focus, tone /// and waiting. The three walks -- this, [`height`] and `crate::focus` -- read /// the same two helpers above so they cannot come apart. fn repeating_body( pass: &mut Pass<'_>, slot: &Slot, repeating: &quasi_router::Repeating, inner: Rect, buf: &mut Buffer, ) -> u16 { let standing = slot.body.len(); let mut used = 0; for (at, placed) in slot.body.iter().enumerate() { // One-based, because it is read by a person. used += crate::node::draw( pass, &Node::section(format!("{} {}", repeating.one, at + 1)), below(inner, used), buf, ); used += crate::node::draw(pass, &placed.node, below(inner, used), buf); if let Some(removes) = removes_of(&placed.node) { let act = Node::Act(bounded(removes, repeating.may_remove(standing))); used += crate::node::draw(pass, &act, below(inner, used), buf); } } let add = Node::Act(bounded(&repeating.add, repeating.may_add(standing))); used += crate::node::draw(pass, &add, below(inner, used), buf); used.min(inner.height) } /// A region showing one child at a time, and the chrome that moves between them. /// /// The same derivation quasi-webview makes and for the same reason: nothing here /// reads [`RegionKind::Widget`]'s name. A carousel, a tab group and a disclosure /// are one region that shows some of its children, and which idiom comes out /// falls out of what the children carry. /// /// This is what `c0b63ea9`'s terminal half was waiting for. /// /// # The chrome is one row, and it is in flow /// /// `< Prev > 2 / 3 < Next >` under the content, or a strip of labels above it. /// The row rather than overlaid arrows or a dot strip: a terminal cannot /// honestly overlay anything, and a dot strip has no form here at all. fn showing_body(pass: &mut Pass<'_>, slot: &Slot, inner: Rect, buf: &mut Buffer) -> u16 { let at = pass.view.shown(slot); let labels = slot.labels(); let mut used = 0; // A strip sits above the panes it opens; a counter row sits under the // content it counts. The folder semantic, and the same placement the // webview derives. if !labels.is_empty() { used += text::draw_spans( &showing_spans(pass.tui, &labels, at), below(inner, used), buf, ); } // One child, or none at all: `Showing::AtMostOne` closed is the only way to // reach `None` here, and drawing nothing is what closed means. if let Some(index) = at && index < slot.body.len() { // The one frame that is up, and nothing else: a selective region draws // its members one at a time, which is what selective means. if let Some(member) = slot.body.get(index) { used += body(pass, slot, &[member], below(inner, used), buf); } } if labels.is_empty() { used += text::draw_spans( &counter_spans(pass.tui, at, slot.body.len()), below(inner, used), buf, ); } used.min(inner.height) } /// A strip of labels, the current one lit. /// /// The one tab strip this renderer draws. fn showing_spans(tui: &Tui, labels: &[&str], at: Option) -> Vec> { let mut spans = Vec::new(); for (index, label) in labels.iter().enumerate() { if !spans.is_empty() { spans.push(Span::raw(" ")); } let picked = at == Some(index); let style = if picked { Style::default() .fg(tui.theme().selection_on) .bg(tui.theme().action_primary) } else { Style::default().fg(tui.theme().content_secondary) }; spans.push(Span::styled(format!(" {label} "), style)); } spans } /// Previous, where you are, next. /// /// The position reads back one step, which is `picture-caption`'s claim in the /// other renderer: it says where you are among the children and it is not one /// of them. Zero when a dismissible region is closed, which is a true statement /// about how many of its children are showing. fn counter_spans(tui: &Tui, at: Option, total: usize) -> Vec> { let control = Style::default().fg(tui.theme().content_secondary); vec![ Span::styled("< Prev >", control), Span::styled( format!(" {} / {total} ", at.map_or(0, |index| index + 1)), Style::default().fg(tui.theme().content_muted), ), Span::styled("< Next >", control), ] } /// A region's contents, at the offset the view is holding it at. /// /// Scrolling is the runtime's and the clipping is the drawing's, and this is /// where the two meet. Flow layout draws from the top of the rect it is given, /// so an offset cannot be honoured by moving the rect: a node starting above /// the window would draw its first row at the window's first row. What works is /// to draw the region at its full height into a buffer of its own and copy the /// window out, which costs an allocation per scrolled region and nothing at all /// for a region sitting at the top, which is every region until someone /// scrolls. /// /// The offset is clamped here rather than in [`crate::View`], because how far a /// region can scroll is how tall it is at the width it was given, and the width /// is not known until this point. /// # What narrowing does here /// /// A member ranked below [`layout::Priority::Essential`] is not drawn once /// [`cutoff`] has risen past it. The cutoff is read off `inner.width` and /// nothing else, which is the whole of "Any width, one answer" in this /// renderer: no count of what fitted, no measurement kept from the last frame. fn body(pass: &mut Pass<'_>, slot: &Slot, nodes: &[&Ranked], inner: Rect, buf: &mut Buffer) -> u16 { let offset = pass.view.scroll(&slot.id); if offset == 0 { let mut used = 0; for node in kept(nodes, inner.width) { used += crate::node::draw(pass, node, below(inner, used), buf); } return used.min(inner.height); } let content: u16 = kept(nodes, inner.width) .map(|node| crate::node::height(pass.tui, node, inner.width, &pass.local())) .sum(); let offset = offset.min(content.saturating_sub(inner.height)); let tall = Rect { height: content.max(inner.height), ..inner }; let mut scratch = Buffer::empty(tall); let mut used = 0; for node in kept(nodes, inner.width) { used += crate::node::draw(pass, node, below(tall, used), &mut scratch); } let shown = inner.height.min(content.saturating_sub(offset)); for row in 0..shown { for column in 0..inner.width { let from = (inner.x + column, inner.y + offset + row); let to = (inner.x + column, inner.y + row); if let Some(cell) = scratch.cell(from).cloned() && let Some(target) = buf.cell_mut(to) { *target = cell; } } } shown } /// Whether a depth is drawn with a border. /// /// Flat is not: a band and a plain pane are arrangement, and boxing every one /// of them spends two rows and two columns per region on a screen that is /// mostly regions. fn framed(depth: layout::Depth) -> bool { !matches!(depth, layout::Depth::Flat) } /// Split `area` into a left column of `width` and the rest. fn split(area: Rect, width: u16) -> (Rect, Rect) { let width = width.min(area.width); ( Rect { width, ..area }, Rect { x: area.x + width, width: area.width - width, ..area }, ) } /// Half the width and half the height, in the middle. fn centred(area: Rect) -> Rect { let width = area.width / 2; let height = area.height / 2; Rect { x: area.x + width / 2, y: area.y + height / 2, width, height, } }