fn render_ref()

in codex-rs/tui/src/conversation_history_widget.rs [238:387]


    fn render_ref(&self, area: Rect, buf: &mut Buffer) {
        let (title, border_style) = if self.has_input_focus {
            (
                "Messages (↑/↓ or j/k = line,  b/space = page)",
                Style::default().fg(Color::LightYellow),
            )
        } else {
            ("Messages (tab to focus)", Style::default().dim())
        };

        let block = Block::default()
            .title(title)
            .borders(Borders::ALL)
            .border_type(BorderType::Rounded)
            .border_style(border_style);

        // ------------------------------------------------------------------
        // Build a *window* into the history instead of cloning the entire
        // history into a brand‑new Vec every time we are asked to render.
        //
        // There can be an unbounded number of `Line` objects in the history,
        // but the terminal will only ever display `height` of them at once.
        // By materialising only the `height` lines that are scrolled into
        // view we avoid the potentially expensive clone of the full
        // conversation every frame.
        // ------------------------------------------------------------------

        // Compute the inner area that will be available for the list after
        // the surrounding `Block` is drawn.
        let inner = block.inner(area);
        let viewport_height = inner.height as usize;

        // Collect the lines that will actually be visible in the viewport
        // while keeping track of the total number of lines so the scrollbar
        // stays correct.
        let num_lines: usize = self.history.iter().map(|c| c.lines().len()).sum();

        let max_scroll = num_lines.saturating_sub(viewport_height) + 1;
        let scroll_pos = if self.scroll_position == usize::MAX {
            max_scroll
        } else {
            self.scroll_position.min(max_scroll)
        };

        let mut visible_lines: Vec<Line<'static>> = Vec::with_capacity(viewport_height);

        if self.scroll_position == usize::MAX {
            // Stick‑to‑bottom mode: walk the history backwards and keep the
            // most recent `height` lines.  This touches at most `height`
            // lines regardless of how large the conversation grows.
            'outer_rev: for cell in self.history.iter().rev() {
                for line in cell.lines().iter().rev() {
                    visible_lines.push(line.clone());
                    if visible_lines.len() == viewport_height {
                        break 'outer_rev;
                    }
                }
            }
            visible_lines.reverse();
        } else {
            // Arbitrary scroll position.  Skip lines until we reach the
            // desired offset, then emit the next `height` lines.
            let start_line = scroll_pos;
            let mut current_index = 0usize;
            'outer_fwd: for cell in &self.history {
                for line in cell.lines() {
                    if current_index >= start_line {
                        visible_lines.push(line.clone());
                        if visible_lines.len() == viewport_height {
                            break 'outer_fwd;
                        }
                    }
                    current_index += 1;
                }
            }
        }

        // We track the number of lines in the struct so can let the user take over from
        // something other than usize::MAX when they start scrolling up. This could be
        // removed once we have the vec<Lines> in self.
        self.num_rendered_lines.set(num_lines);
        self.last_viewport_height.set(viewport_height);

        // The widget takes care of drawing the `block` and computing its own
        // inner area, so we render it over the full `area`.
        // We *manually* sliced the set of `visible_lines` to fit within the
        // viewport above, so there is no need to ask the `Paragraph` widget
        // to apply an additional scroll offset. Doing so would cause the
        // content to be shifted *twice* – once by our own logic and then a
        // second time by the widget – which manifested as the entire block
        // drifting off‑screen when the user attempted to scroll.

        let paragraph = Paragraph::new(visible_lines)
            .block(block)
            .wrap(Wrap { trim: false });
        paragraph.render(area, buf);

        let needs_scrollbar = num_lines > viewport_height;
        if needs_scrollbar {
            let mut scroll_state = ScrollbarState::default()
                // TODO(ragona):
                // I don't totally understand this, but it appears to work exactly as expected
                // if we set the content length as the lines minus the height. Maybe I was supposed
                // to use viewport_content_length or something, but this works and I'm backing away.
                .content_length(num_lines.saturating_sub(viewport_height))
                .position(scroll_pos);

            // Choose a thumb colour that stands out only when this pane has focus so that the
            // user’s attention is naturally drawn to the active viewport. When unfocused we show
            // a low‑contrast thumb so the scrollbar fades into the background without becoming
            // invisible.

            let thumb_style = if self.has_input_focus {
                Style::reset().fg(Color::LightYellow)
            } else {
                Style::reset().fg(Color::Gray)
            };

            StatefulWidget::render(
                // By default the Scrollbar widget inherits the style that was already present
                // in the underlying buffer cells.  That means if a coloured line (for example a
                // background task notification that we render in blue) happens to be underneath
                // the scrollbar, the track and thumb adopt that colour and the scrollbar appears
                // to “change colour”.  Explicitly setting the *track* and *thumb* styles ensures
                // we always draw the scrollbar with the same palette regardless of what content
                // is behind it.
                //
                // N.B.  Only the *foreground* colour matters here because the scrollbar symbols
                // themselves are filled‐in block glyphs that completely overwrite the prior
                // character cells.  We therefore leave the background at its default value so it
                // blends nicely with the surrounding `Block`.
                Scrollbar::new(ScrollbarOrientation::VerticalRight)
                    .begin_symbol(Some("↑"))
                    .end_symbol(Some("↓"))
                    .begin_style(Style::reset().fg(Color::DarkGray))
                    .end_style(Style::reset().fg(Color::DarkGray))
                    // A solid thumb so that we can colour it distinctly from the track.
                    .thumb_symbol("█")
                    // Apply the dynamic thumb colour computed above. We still start from
                    // Style::reset() to clear any inherited modifiers.
                    .thumb_style(thumb_style)
                    // Thin vertical line for the track.
                    .track_symbol(Some("│"))
                    .track_style(Style::reset().fg(Color::DarkGray)),
                inner,
                buf,
                &mut scroll_state,
            );
        }
    }