mirror of
https://github.com/openai/codex.git
synced 2026-05-03 04:42:20 +03:00
feat(tui2): copy tui crate and normalize snapshots (#7833)
Introduce a full codex-tui source snapshot under the new codex-tui2 crate so viewport work can be replayed in isolation. This change copies the entire codex-rs/tui/src tree into codex-rs/tui2/src in one atomic step, rather than piecemeal, to keep future diffs vs the original viewport bookmark easy to reason about. The goal is for codex-tui2 to render identically to the existing TUI behind the `features.tui2` flag while we gradually port the viewport/history commits from the joshka/viewport bookmark onto this forked tree. While on this baseline change, we also ran the codex-tui2 snapshot test suite and accepted all insta snapshots for the new crate, so the snapshot files now use the codex-tui2 naming scheme and encode the unmodified legacy TUI behavior. This keeps later viewport commits focused on intentional behavior changes (and their snapshots) rather than on mechanical snapshot renames.
This commit is contained in:
236
codex-rs/tui2/src/render/highlight.rs
Normal file
236
codex-rs/tui2/src/render/highlight.rs
Normal file
@@ -0,0 +1,236 @@
|
||||
use ratatui::style::Style;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::text::Span;
|
||||
use std::sync::OnceLock;
|
||||
use tree_sitter_highlight::Highlight;
|
||||
use tree_sitter_highlight::HighlightConfiguration;
|
||||
use tree_sitter_highlight::HighlightEvent;
|
||||
use tree_sitter_highlight::Highlighter;
|
||||
|
||||
// Ref: https://github.com/tree-sitter/tree-sitter-bash/blob/master/queries/highlights.scm
|
||||
#[derive(Copy, Clone)]
|
||||
enum BashHighlight {
|
||||
Comment,
|
||||
Constant,
|
||||
Embedded,
|
||||
Function,
|
||||
Keyword,
|
||||
Number,
|
||||
Operator,
|
||||
Property,
|
||||
String,
|
||||
}
|
||||
|
||||
impl BashHighlight {
|
||||
const ALL: [Self; 9] = [
|
||||
Self::Comment,
|
||||
Self::Constant,
|
||||
Self::Embedded,
|
||||
Self::Function,
|
||||
Self::Keyword,
|
||||
Self::Number,
|
||||
Self::Operator,
|
||||
Self::Property,
|
||||
Self::String,
|
||||
];
|
||||
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Comment => "comment",
|
||||
Self::Constant => "constant",
|
||||
Self::Embedded => "embedded",
|
||||
Self::Function => "function",
|
||||
Self::Keyword => "keyword",
|
||||
Self::Number => "number",
|
||||
Self::Operator => "operator",
|
||||
Self::Property => "property",
|
||||
Self::String => "string",
|
||||
}
|
||||
}
|
||||
|
||||
fn style(self) -> Style {
|
||||
match self {
|
||||
Self::Comment | Self::Operator | Self::String => Style::default().dim(),
|
||||
_ => Style::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static HIGHLIGHT_CONFIG: OnceLock<HighlightConfiguration> = OnceLock::new();
|
||||
|
||||
fn highlight_names() -> &'static [&'static str] {
|
||||
static NAMES: OnceLock<[&'static str; BashHighlight::ALL.len()]> = OnceLock::new();
|
||||
NAMES
|
||||
.get_or_init(|| BashHighlight::ALL.map(BashHighlight::as_str))
|
||||
.as_slice()
|
||||
}
|
||||
|
||||
fn highlight_config() -> &'static HighlightConfiguration {
|
||||
HIGHLIGHT_CONFIG.get_or_init(|| {
|
||||
let language = tree_sitter_bash::LANGUAGE.into();
|
||||
#[expect(clippy::expect_used)]
|
||||
let mut config = HighlightConfiguration::new(
|
||||
language,
|
||||
"bash",
|
||||
tree_sitter_bash::HIGHLIGHT_QUERY,
|
||||
"",
|
||||
"",
|
||||
)
|
||||
.expect("load bash highlight query");
|
||||
config.configure(highlight_names());
|
||||
config
|
||||
})
|
||||
}
|
||||
|
||||
fn highlight_for(highlight: Highlight) -> BashHighlight {
|
||||
BashHighlight::ALL[highlight.0]
|
||||
}
|
||||
|
||||
fn push_segment(lines: &mut Vec<Line<'static>>, segment: &str, style: Option<Style>) {
|
||||
for (i, part) in segment.split('\n').enumerate() {
|
||||
if i > 0 {
|
||||
lines.push(Line::from(""));
|
||||
}
|
||||
if part.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let span = match style {
|
||||
Some(style) => Span::styled(part.to_string(), style),
|
||||
None => part.to_string().into(),
|
||||
};
|
||||
if let Some(last) = lines.last_mut() {
|
||||
last.spans.push(span);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a bash script into per-line styled content using tree-sitter's
|
||||
/// bash highlight query. The highlighter is streamed so multi-line content is
|
||||
/// split into `Line`s while preserving style boundaries.
|
||||
pub(crate) fn highlight_bash_to_lines(script: &str) -> Vec<Line<'static>> {
|
||||
let mut highlighter = Highlighter::new();
|
||||
let iterator =
|
||||
match highlighter.highlight(highlight_config(), script.as_bytes(), None, |_| None) {
|
||||
Ok(iter) => iter,
|
||||
Err(_) => return vec![script.to_string().into()],
|
||||
};
|
||||
|
||||
let mut lines: Vec<Line<'static>> = vec![Line::from("")];
|
||||
let mut highlight_stack: Vec<Highlight> = Vec::new();
|
||||
|
||||
for event in iterator {
|
||||
match event {
|
||||
Ok(HighlightEvent::HighlightStart(highlight)) => highlight_stack.push(highlight),
|
||||
Ok(HighlightEvent::HighlightEnd) => {
|
||||
highlight_stack.pop();
|
||||
}
|
||||
Ok(HighlightEvent::Source { start, end }) => {
|
||||
if start == end {
|
||||
continue;
|
||||
}
|
||||
let style = highlight_stack.last().map(|h| highlight_for(*h).style());
|
||||
push_segment(&mut lines, &script[start..end], style);
|
||||
}
|
||||
Err(_) => return vec![script.to_string().into()],
|
||||
}
|
||||
}
|
||||
|
||||
if lines.is_empty() {
|
||||
vec![Line::from("")]
|
||||
} else {
|
||||
lines
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use ratatui::style::Modifier;
|
||||
|
||||
fn reconstructed(lines: &[Line<'static>]) -> String {
|
||||
lines
|
||||
.iter()
|
||||
.map(|l| {
|
||||
l.spans
|
||||
.iter()
|
||||
.map(|sp| sp.content.clone())
|
||||
.collect::<String>()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
fn dimmed_tokens(lines: &[Line<'static>]) -> Vec<String> {
|
||||
lines
|
||||
.iter()
|
||||
.flat_map(|l| l.spans.iter())
|
||||
.filter(|sp| sp.style.add_modifier.contains(Modifier::DIM))
|
||||
.map(|sp| sp.content.clone().into_owned())
|
||||
.map(|token| token.trim().to_string())
|
||||
.filter(|token| !token.is_empty())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dims_expected_bash_operators() {
|
||||
let s = "echo foo && bar || baz | qux & (echo hi)";
|
||||
let lines = highlight_bash_to_lines(s);
|
||||
assert_eq!(reconstructed(&lines), s);
|
||||
|
||||
let dimmed = dimmed_tokens(&lines);
|
||||
assert!(dimmed.contains(&"&&".to_string()));
|
||||
assert!(dimmed.contains(&"|".to_string()));
|
||||
assert!(!dimmed.contains(&"echo".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dims_redirects_and_strings() {
|
||||
let s = "echo \"hi\" > out.txt; echo 'ok'";
|
||||
let lines = highlight_bash_to_lines(s);
|
||||
assert_eq!(reconstructed(&lines), s);
|
||||
|
||||
let dimmed = dimmed_tokens(&lines);
|
||||
assert!(dimmed.contains(&">".to_string()));
|
||||
assert!(dimmed.contains(&"\"hi\"".to_string()));
|
||||
assert!(dimmed.contains(&"'ok'".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn highlights_command_and_strings() {
|
||||
let s = "echo \"hi\"";
|
||||
let lines = highlight_bash_to_lines(s);
|
||||
let mut echo_style = None;
|
||||
let mut string_style = None;
|
||||
for span in &lines[0].spans {
|
||||
let text = span.content.as_ref();
|
||||
if text == "echo" {
|
||||
echo_style = Some(span.style);
|
||||
}
|
||||
if text == "\"hi\"" {
|
||||
string_style = Some(span.style);
|
||||
}
|
||||
}
|
||||
let echo_style = echo_style.expect("echo span missing");
|
||||
let string_style = string_style.expect("string span missing");
|
||||
assert!(echo_style.fg.is_none());
|
||||
assert!(!echo_style.add_modifier.contains(Modifier::DIM));
|
||||
assert!(string_style.add_modifier.contains(Modifier::DIM));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn highlights_heredoc_body_as_string() {
|
||||
let s = "cat <<EOF\nheredoc body\nEOF";
|
||||
let lines = highlight_bash_to_lines(s);
|
||||
let body_line = &lines[1];
|
||||
let mut body_style = None;
|
||||
for span in &body_line.spans {
|
||||
if span.content.as_ref() == "heredoc body" {
|
||||
body_style = Some(span.style);
|
||||
}
|
||||
}
|
||||
let body_style = body_style.expect("missing heredoc span");
|
||||
assert!(body_style.add_modifier.contains(Modifier::DIM));
|
||||
}
|
||||
}
|
||||
59
codex-rs/tui2/src/render/line_utils.rs
Normal file
59
codex-rs/tui2/src/render/line_utils.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use ratatui::text::Line;
|
||||
use ratatui::text::Span;
|
||||
|
||||
/// Clone a borrowed ratatui `Line` into an owned `'static` line.
|
||||
pub fn line_to_static(line: &Line<'_>) -> Line<'static> {
|
||||
Line {
|
||||
style: line.style,
|
||||
alignment: line.alignment,
|
||||
spans: line
|
||||
.spans
|
||||
.iter()
|
||||
.map(|s| Span {
|
||||
style: s.style,
|
||||
content: std::borrow::Cow::Owned(s.content.to_string()),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Append owned copies of borrowed lines to `out`.
|
||||
pub fn push_owned_lines<'a>(src: &[Line<'a>], out: &mut Vec<Line<'static>>) {
|
||||
for l in src {
|
||||
out.push(line_to_static(l));
|
||||
}
|
||||
}
|
||||
|
||||
/// Consider a line blank if it has no spans or only spans whose contents are
|
||||
/// empty or consist solely of spaces (no tabs/newlines).
|
||||
pub fn is_blank_line_spaces_only(line: &Line<'_>) -> bool {
|
||||
if line.spans.is_empty() {
|
||||
return true;
|
||||
}
|
||||
line.spans
|
||||
.iter()
|
||||
.all(|s| s.content.is_empty() || s.content.chars().all(|c| c == ' '))
|
||||
}
|
||||
|
||||
/// Prefix each line with `initial_prefix` for the first line and
|
||||
/// `subsequent_prefix` for following lines. Returns a new Vec of owned lines.
|
||||
pub fn prefix_lines(
|
||||
lines: Vec<Line<'static>>,
|
||||
initial_prefix: Span<'static>,
|
||||
subsequent_prefix: Span<'static>,
|
||||
) -> Vec<Line<'static>> {
|
||||
lines
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, l)| {
|
||||
let mut spans = Vec::with_capacity(l.spans.len() + 1);
|
||||
spans.push(if i == 0 {
|
||||
initial_prefix.clone()
|
||||
} else {
|
||||
subsequent_prefix.clone()
|
||||
});
|
||||
spans.extend(l.spans);
|
||||
Line::from(spans).style(l.style)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
50
codex-rs/tui2/src/render/mod.rs
Normal file
50
codex-rs/tui2/src/render/mod.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use ratatui::layout::Rect;
|
||||
|
||||
pub mod highlight;
|
||||
pub mod line_utils;
|
||||
pub mod renderable;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Insets {
|
||||
left: u16,
|
||||
top: u16,
|
||||
right: u16,
|
||||
bottom: u16,
|
||||
}
|
||||
|
||||
impl Insets {
|
||||
pub fn tlbr(top: u16, left: u16, bottom: u16, right: u16) -> Self {
|
||||
Self {
|
||||
top,
|
||||
left,
|
||||
bottom,
|
||||
right,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vh(v: u16, h: u16) -> Self {
|
||||
Self {
|
||||
top: v,
|
||||
left: h,
|
||||
bottom: v,
|
||||
right: h,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RectExt {
|
||||
fn inset(&self, insets: Insets) -> Rect;
|
||||
}
|
||||
|
||||
impl RectExt for Rect {
|
||||
fn inset(&self, insets: Insets) -> Rect {
|
||||
let horizontal = insets.left.saturating_add(insets.right);
|
||||
let vertical = insets.top.saturating_add(insets.bottom);
|
||||
Rect {
|
||||
x: self.x.saturating_add(insets.left),
|
||||
y: self.y.saturating_add(insets.top),
|
||||
width: self.width.saturating_sub(horizontal),
|
||||
height: self.height.saturating_sub(vertical),
|
||||
}
|
||||
}
|
||||
}
|
||||
431
codex-rs/tui2/src/render/renderable.rs
Normal file
431
codex-rs/tui2/src/render/renderable.rs
Normal file
@@ -0,0 +1,431 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::text::Span;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::widgets::WidgetRef;
|
||||
|
||||
use crate::render::Insets;
|
||||
use crate::render::RectExt as _;
|
||||
|
||||
pub trait Renderable {
|
||||
fn render(&self, area: Rect, buf: &mut Buffer);
|
||||
fn desired_height(&self, width: u16) -> u16;
|
||||
fn cursor_pos(&self, _area: Rect) -> Option<(u16, u16)> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub enum RenderableItem<'a> {
|
||||
Owned(Box<dyn Renderable + 'a>),
|
||||
Borrowed(&'a dyn Renderable),
|
||||
}
|
||||
|
||||
impl<'a> Renderable for RenderableItem<'a> {
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
match self {
|
||||
RenderableItem::Owned(child) => child.render(area, buf),
|
||||
RenderableItem::Borrowed(child) => child.render(area, buf),
|
||||
}
|
||||
}
|
||||
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
match self {
|
||||
RenderableItem::Owned(child) => child.desired_height(width),
|
||||
RenderableItem::Borrowed(child) => child.desired_height(width),
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
|
||||
match self {
|
||||
RenderableItem::Owned(child) => child.cursor_pos(area),
|
||||
RenderableItem::Borrowed(child) => child.cursor_pos(area),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Box<dyn Renderable + 'a>> for RenderableItem<'a> {
|
||||
fn from(value: Box<dyn Renderable + 'a>) -> Self {
|
||||
RenderableItem::Owned(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, R> From<R> for Box<dyn Renderable + 'a>
|
||||
where
|
||||
R: Renderable + 'a,
|
||||
{
|
||||
fn from(value: R) -> Self {
|
||||
Box::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderable for () {
|
||||
fn render(&self, _area: Rect, _buf: &mut Buffer) {}
|
||||
fn desired_height(&self, _width: u16) -> u16 {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderable for &str {
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
}
|
||||
fn desired_height(&self, _width: u16) -> u16 {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderable for String {
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
}
|
||||
fn desired_height(&self, _width: u16) -> u16 {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Renderable for Span<'a> {
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
}
|
||||
fn desired_height(&self, _width: u16) -> u16 {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Renderable for Line<'a> {
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
WidgetRef::render_ref(self, area, buf);
|
||||
}
|
||||
fn desired_height(&self, _width: u16) -> u16 {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Renderable for Paragraph<'a> {
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
}
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
self.line_count(width) as u16
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Renderable> Renderable for Option<R> {
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
if let Some(renderable) = self {
|
||||
renderable.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
if let Some(renderable) = self {
|
||||
renderable.desired_height(width)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Renderable> Renderable for Arc<R> {
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
self.as_ref().render(area, buf);
|
||||
}
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
self.as_ref().desired_height(width)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ColumnRenderable<'a> {
|
||||
children: Vec<RenderableItem<'a>>,
|
||||
}
|
||||
|
||||
impl Renderable for ColumnRenderable<'_> {
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
let mut y = area.y;
|
||||
for child in &self.children {
|
||||
let child_area = Rect::new(area.x, y, area.width, child.desired_height(area.width))
|
||||
.intersection(area);
|
||||
if !child_area.is_empty() {
|
||||
child.render(child_area, buf);
|
||||
}
|
||||
y += child_area.height;
|
||||
}
|
||||
}
|
||||
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
self.children
|
||||
.iter()
|
||||
.map(|child| child.desired_height(width))
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Returns the cursor position of the first child that has a cursor position, offset by the
|
||||
/// child's position in the column.
|
||||
///
|
||||
/// It is generally assumed that either zero or one child will have a cursor position.
|
||||
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
|
||||
let mut y = area.y;
|
||||
for child in &self.children {
|
||||
let child_area = Rect::new(area.x, y, area.width, child.desired_height(area.width))
|
||||
.intersection(area);
|
||||
if !child_area.is_empty()
|
||||
&& let Some((px, py)) = child.cursor_pos(child_area)
|
||||
{
|
||||
return Some((px, py));
|
||||
}
|
||||
y += child_area.height;
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ColumnRenderable<'a> {
|
||||
pub fn new() -> Self {
|
||||
Self { children: vec![] }
|
||||
}
|
||||
|
||||
pub fn with<I, T>(children: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: Into<RenderableItem<'a>>,
|
||||
{
|
||||
Self {
|
||||
children: children.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, child: impl Into<Box<dyn Renderable + 'a>>) {
|
||||
self.children.push(RenderableItem::Owned(child.into()));
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn push_ref<R>(&mut self, child: &'a R)
|
||||
where
|
||||
R: Renderable + 'a,
|
||||
{
|
||||
self.children
|
||||
.push(RenderableItem::Borrowed(child as &'a dyn Renderable));
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FlexChild<'a> {
|
||||
flex: i32,
|
||||
child: RenderableItem<'a>,
|
||||
}
|
||||
|
||||
pub struct FlexRenderable<'a> {
|
||||
children: Vec<FlexChild<'a>>,
|
||||
}
|
||||
|
||||
/// Lays out children in a column, with the ability to specify a flex factor for each child.
|
||||
///
|
||||
/// Children with flex factor > 0 will be allocated the remaining space after the non-flex children,
|
||||
/// proportional to the flex factor.
|
||||
impl<'a> FlexRenderable<'a> {
|
||||
pub fn new() -> Self {
|
||||
Self { children: vec![] }
|
||||
}
|
||||
|
||||
pub fn push(&mut self, flex: i32, child: impl Into<RenderableItem<'a>>) {
|
||||
self.children.push(FlexChild {
|
||||
flex,
|
||||
child: child.into(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Loosely inspired by Flutter's Flex widget.
|
||||
///
|
||||
/// Ref https://github.com/flutter/flutter/blob/3fd81edbf1e015221e143c92b2664f4371bdc04a/packages/flutter/lib/src/rendering/flex.dart#L1205-L1209
|
||||
fn allocate(&self, area: Rect) -> Vec<Rect> {
|
||||
let mut allocated_rects = Vec::with_capacity(self.children.len());
|
||||
let mut child_sizes = vec![0; self.children.len()];
|
||||
let mut allocated_size = 0;
|
||||
let mut total_flex = 0;
|
||||
|
||||
// 1. Allocate space to non-flex children.
|
||||
let max_size = area.height;
|
||||
let mut last_flex_child_idx = 0;
|
||||
for (i, FlexChild { flex, child }) in self.children.iter().enumerate() {
|
||||
if *flex > 0 {
|
||||
total_flex += flex;
|
||||
last_flex_child_idx = i;
|
||||
} else {
|
||||
child_sizes[i] = child
|
||||
.desired_height(area.width)
|
||||
.min(max_size.saturating_sub(allocated_size));
|
||||
allocated_size += child_sizes[i];
|
||||
}
|
||||
}
|
||||
let free_space = max_size.saturating_sub(allocated_size);
|
||||
// 2. Allocate space to flex children, proportional to their flex factor.
|
||||
let mut allocated_flex_space = 0;
|
||||
if total_flex > 0 {
|
||||
let space_per_flex = free_space / total_flex as u16;
|
||||
for (i, FlexChild { flex, child }) in self.children.iter().enumerate() {
|
||||
if *flex > 0 {
|
||||
// Last flex child gets all the remaining space, to prevent a rounding error
|
||||
// from not allocating all the space.
|
||||
let max_child_extent = if i == last_flex_child_idx {
|
||||
free_space - allocated_flex_space
|
||||
} else {
|
||||
space_per_flex * *flex as u16
|
||||
};
|
||||
let child_size = child.desired_height(area.width).min(max_child_extent);
|
||||
child_sizes[i] = child_size;
|
||||
allocated_size += child_size;
|
||||
allocated_flex_space += child_size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut y = area.y;
|
||||
for size in child_sizes {
|
||||
let child_area = Rect::new(area.x, y, area.width, size);
|
||||
allocated_rects.push(child_area);
|
||||
y += child_area.height;
|
||||
}
|
||||
allocated_rects
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Renderable for FlexRenderable<'a> {
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
self.allocate(area)
|
||||
.into_iter()
|
||||
.zip(self.children.iter())
|
||||
.for_each(|(rect, child)| {
|
||||
child.child.render(rect, buf);
|
||||
});
|
||||
}
|
||||
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
self.allocate(Rect::new(0, 0, width, u16::MAX))
|
||||
.last()
|
||||
.map(|rect| rect.bottom())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
|
||||
self.allocate(area)
|
||||
.into_iter()
|
||||
.zip(self.children.iter())
|
||||
.find_map(|(rect, child)| child.child.cursor_pos(rect))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RowRenderable<'a> {
|
||||
children: Vec<(u16, RenderableItem<'a>)>,
|
||||
}
|
||||
|
||||
impl Renderable for RowRenderable<'_> {
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
let mut x = area.x;
|
||||
for (width, child) in &self.children {
|
||||
let available_width = area.width.saturating_sub(x - area.x);
|
||||
let child_area = Rect::new(x, area.y, (*width).min(available_width), area.height);
|
||||
if child_area.is_empty() {
|
||||
break;
|
||||
}
|
||||
child.render(child_area, buf);
|
||||
x = x.saturating_add(*width);
|
||||
}
|
||||
}
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
let mut max_height = 0;
|
||||
let mut width_remaining = width;
|
||||
for (child_width, child) in &self.children {
|
||||
let w = (*child_width).min(width_remaining);
|
||||
if w == 0 {
|
||||
break;
|
||||
}
|
||||
let height = child.desired_height(w);
|
||||
if height > max_height {
|
||||
max_height = height;
|
||||
}
|
||||
width_remaining = width_remaining.saturating_sub(w);
|
||||
}
|
||||
max_height
|
||||
}
|
||||
|
||||
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
|
||||
let mut x = area.x;
|
||||
for (width, child) in &self.children {
|
||||
let available_width = area.width.saturating_sub(x - area.x);
|
||||
let child_area = Rect::new(x, area.y, (*width).min(available_width), area.height);
|
||||
if !child_area.is_empty()
|
||||
&& let Some(pos) = child.cursor_pos(child_area)
|
||||
{
|
||||
return Some(pos);
|
||||
}
|
||||
x = x.saturating_add(*width);
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> RowRenderable<'a> {
|
||||
pub fn new() -> Self {
|
||||
Self { children: vec![] }
|
||||
}
|
||||
|
||||
pub fn push(&mut self, width: u16, child: impl Into<Box<dyn Renderable>>) {
|
||||
self.children
|
||||
.push((width, RenderableItem::Owned(child.into())));
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn push_ref<R>(&mut self, width: u16, child: &'a R)
|
||||
where
|
||||
R: Renderable + 'a,
|
||||
{
|
||||
self.children
|
||||
.push((width, RenderableItem::Borrowed(child as &'a dyn Renderable)));
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InsetRenderable<'a> {
|
||||
child: RenderableItem<'a>,
|
||||
insets: Insets,
|
||||
}
|
||||
|
||||
impl<'a> Renderable for InsetRenderable<'a> {
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
self.child.render(area.inset(self.insets), buf);
|
||||
}
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
self.child
|
||||
.desired_height(width - self.insets.left - self.insets.right)
|
||||
+ self.insets.top
|
||||
+ self.insets.bottom
|
||||
}
|
||||
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
|
||||
self.child.cursor_pos(area.inset(self.insets))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> InsetRenderable<'a> {
|
||||
pub fn new(child: impl Into<RenderableItem<'a>>, insets: Insets) -> Self {
|
||||
Self {
|
||||
child: child.into(),
|
||||
insets,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RenderableExt<'a> {
|
||||
fn inset(self, insets: Insets) -> RenderableItem<'a>;
|
||||
}
|
||||
|
||||
impl<'a, R> RenderableExt<'a> for R
|
||||
where
|
||||
R: Renderable + 'a,
|
||||
{
|
||||
fn inset(self, insets: Insets) -> RenderableItem<'a> {
|
||||
let child: RenderableItem<'a> =
|
||||
RenderableItem::Owned(Box::new(self) as Box<dyn Renderable + 'a>);
|
||||
RenderableItem::Owned(Box::new(InsetRenderable { child, insets }))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user