Codex Handbook
tui/src/bottom_pane/request_user_input/layout.rs 363 lines
use ratatui::layout::Rect;use super::DESIRED_SPACERS_BETWEEN_SECTIONS;use super::RequestUserInputOverlay;pub(super) struct LayoutSections {    pub(super) progress_area: Rect,    pub(super) question_area: Rect,    // Wrapped question text lines to render in the question area.    pub(super) question_lines: Vec<String>,    pub(super) options_area: Rect,    pub(super) notes_area: Rect,    // Number of footer rows (status + hints).    pub(super) footer_lines: u16,}impl RequestUserInputOverlay {    /// Compute layout sections, collapsing notes and hints as space shrinks.    pub(super) fn layout_sections(&self, area: Rect) -> LayoutSections {        let has_options = self.has_options();        let notes_visible = !has_options || self.notes_ui_visible();        let footer_pref = self.footer_required_height(area.width);        let notes_pref_height = self.notes_input_height(area.width);        let mut question_lines = self.wrapped_question_lines(area.width);        let question_height = question_lines.len() as u16;        let layout = if has_options {            self.layout_with_options(                OptionsLayoutArgs {                    available_height: area.height,                    width: area.width,                    question_height,                    notes_pref_height,                    footer_pref,                    notes_visible,                },                &mut question_lines,            )        } else {            self.layout_without_options(                area.height,                question_height,                notes_pref_height,                footer_pref,                &mut question_lines,            )        };        let (progress_area, question_area, options_area, notes_area) =            self.build_layout_areas(area, layout);        LayoutSections {            progress_area,            question_area,            question_lines,            options_area,            notes_area,            footer_lines: layout.footer_lines,        }    }    /// Layout calculation when options are present.    fn layout_with_options(        &self,        args: OptionsLayoutArgs,        question_lines: &mut Vec<String>,    ) -> LayoutPlan {        let OptionsLayoutArgs {            available_height,            width,            mut question_height,            notes_pref_height,            footer_pref,            notes_visible,        } = args;        let min_options_height = available_height.min(1);        let max_question_height = available_height.saturating_sub(min_options_height);        if question_height > max_question_height {            question_height = max_question_height;            question_lines.truncate(question_height as usize);        }        self.layout_with_options_normal(            OptionsNormalArgs {                available_height,                question_height,                notes_pref_height,                footer_pref,                notes_visible,            },            OptionsHeights {                preferred: self.options_preferred_height(width),                full: self.options_required_height(width),            },        )    }    /// Normal layout for options case: allocate footer + progress first, and    /// only allocate notes (and its label) when explicitly visible.    fn layout_with_options_normal(        &self,        args: OptionsNormalArgs,        options: OptionsHeights,    ) -> LayoutPlan {        let OptionsNormalArgs {            available_height,            question_height,            notes_pref_height,            footer_pref,            notes_visible,        } = args;        let max_options_height = available_height.saturating_sub(question_height);        let min_options_height = max_options_height.min(1);        let mut options_height = options            .preferred            .min(max_options_height)            .max(min_options_height);        let used = question_height.saturating_add(options_height);        let mut remaining = available_height.saturating_sub(used);        // When notes are hidden, prefer to reserve room for progress, footer,        // and spacers by shrinking the options window if needed.        let desired_spacers = if notes_visible {            // Notes already separate options from the footer, so only keep a            // single spacer between the question and options.            1        } else {            DESIRED_SPACERS_BETWEEN_SECTIONS        };        let required_extra = footer_pref            .saturating_add(1) // progress line            .saturating_add(desired_spacers);        if remaining < required_extra {            let deficit = required_extra.saturating_sub(remaining);            let reducible = options_height.saturating_sub(min_options_height);            let reduce_by = deficit.min(reducible);            options_height = options_height.saturating_sub(reduce_by);            remaining = remaining.saturating_add(reduce_by);        }        let mut progress_height = 0;        if remaining > 0 {            progress_height = 1;            remaining = remaining.saturating_sub(1);        }        if !notes_visible {            let mut spacer_after_options = 0;            if remaining > footer_pref {                spacer_after_options = 1;                remaining = remaining.saturating_sub(1);            }            let footer_lines = footer_pref.min(remaining);            remaining = remaining.saturating_sub(footer_lines);            let mut spacer_after_question = 0;            if remaining > 0 {                spacer_after_question = 1;                remaining = remaining.saturating_sub(1);            }            let grow_by = remaining.min(options.full.saturating_sub(options_height));            options_height = options_height.saturating_add(grow_by);            return LayoutPlan {                question_height,                progress_height,                spacer_after_question,                options_height,                spacer_after_options,                notes_height: 0,                footer_lines,            };        }        let footer_lines = footer_pref.min(remaining);        remaining = remaining.saturating_sub(footer_lines);        // Prefer spacers before notes, then notes.        let mut spacer_after_question = 0;        if remaining > 0 {            spacer_after_question = 1;            remaining = remaining.saturating_sub(1);        }        let spacer_after_options = 0;        let mut notes_height = notes_pref_height.min(remaining);        remaining = remaining.saturating_sub(notes_height);        notes_height = notes_height.saturating_add(remaining);        LayoutPlan {            question_height,            progress_height,            spacer_after_question,            options_height,            spacer_after_options,            notes_height,            footer_lines,        }    }    /// Layout calculation when no options are present.    ///    /// Handles both tight layout (when space is constrained) and normal layout    /// (when there's sufficient space for all elements).    ///    fn layout_without_options(        &self,        available_height: u16,        question_height: u16,        notes_pref_height: u16,        footer_pref: u16,        question_lines: &mut Vec<String>,    ) -> LayoutPlan {        let required = question_height;        if required > available_height {            self.layout_without_options_tight(available_height, question_height, question_lines)        } else {            self.layout_without_options_normal(                available_height,                question_height,                notes_pref_height,                footer_pref,            )        }    }    /// Tight layout for no-options case: truncate question to fit available space.    fn layout_without_options_tight(        &self,        available_height: u16,        question_height: u16,        question_lines: &mut Vec<String>,    ) -> LayoutPlan {        let max_question_height = available_height;        let adjusted_question_height = question_height.min(max_question_height);        question_lines.truncate(adjusted_question_height as usize);        LayoutPlan {            question_height: adjusted_question_height,            progress_height: 0,            spacer_after_question: 0,            options_height: 0,            spacer_after_options: 0,            notes_height: 0,            footer_lines: 0,        }    }    /// Normal layout for no-options case: allocate space for notes, footer, and progress.    fn layout_without_options_normal(        &self,        available_height: u16,        question_height: u16,        notes_pref_height: u16,        footer_pref: u16,    ) -> LayoutPlan {        let required = question_height;        let mut remaining = available_height.saturating_sub(required);        let mut notes_height = notes_pref_height.min(remaining);        remaining = remaining.saturating_sub(notes_height);        let footer_lines = footer_pref.min(remaining);        remaining = remaining.saturating_sub(footer_lines);        let mut progress_height = 0;        if remaining > 0 {            progress_height = 1;            remaining = remaining.saturating_sub(1);        }        notes_height = notes_height.saturating_add(remaining);        LayoutPlan {            question_height,            progress_height,            spacer_after_question: 0,            options_height: 0,            spacer_after_options: 0,            notes_height,            footer_lines,        }    }    /// Build the final layout areas from computed heights.    fn build_layout_areas(        &self,        area: Rect,        heights: LayoutPlan,    ) -> (        Rect, // progress_area        Rect, // question_area        Rect, // options_area        Rect, // notes_area    ) {        let mut cursor_y = area.y;        let progress_area = Rect {            x: area.x,            y: cursor_y,            width: area.width,            height: heights.progress_height,        };        cursor_y = cursor_y.saturating_add(heights.progress_height);        let question_area = Rect {            x: area.x,            y: cursor_y,            width: area.width,            height: heights.question_height,        };        cursor_y = cursor_y.saturating_add(heights.question_height);        cursor_y = cursor_y.saturating_add(heights.spacer_after_question);        let options_area = Rect {            x: area.x,            y: cursor_y,            width: area.width,            height: heights.options_height,        };        cursor_y = cursor_y.saturating_add(heights.options_height);        cursor_y = cursor_y.saturating_add(heights.spacer_after_options);        let notes_area = Rect {            x: area.x,            y: cursor_y,            width: area.width,            height: heights.notes_height,        };        (progress_area, question_area, options_area, notes_area)    }}#[derive(Clone, Copy, Debug)]struct LayoutPlan {    progress_height: u16,    question_height: u16,    spacer_after_question: u16,    options_height: u16,    spacer_after_options: u16,    notes_height: u16,    footer_lines: u16,}#[derive(Clone, Copy, Debug)]struct OptionsLayoutArgs {    available_height: u16,    width: u16,    question_height: u16,    notes_pref_height: u16,    footer_pref: u16,    notes_visible: bool,}#[derive(Clone, Copy, Debug)]struct OptionsNormalArgs {    available_height: u16,    question_height: u16,    notes_pref_height: u16,    footer_pref: u16,    notes_visible: bool,}#[derive(Clone, Copy, Debug)]struct OptionsHeights {    preferred: u16,    full: u16,}