返回 CodeWhale
composer_chrome.rs
根目录 / crates / tui / src / tui / composer_chrome.rs
1 //! Ocean composer chrome policy.
2 //!
3 //! The composer auto-fits its content: one input row when empty or
4 //! single-line, growing with typed content up to the density cap. Density
5 //! no longer forces a multi-row baseline — it only bounds how tall the
6 //! composer may grow. Content-driven growth still wins once the user
7 //! types past one row, and submit/clear collapses the composer back to
8 //! a single input row.
9
10 use crate::tui::app::ComposerDensity;
11
12 /// Top/bottom chrome rows for the quiet rule (TOP border only) or the
13 /// enclosed panel (TOP + BOTTOM), plus the total-row growth cap.
14 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
15 pub struct ComposerChrome {
16 pub border_rows: u16,
17 pub max_total_rows: u16,
18 }
19
20 impl ComposerChrome {
21 /// Baseline for the given density. Panel shape gets both borders;
22 /// quiet shape keeps a single top rule so the prompt still has a
23 /// clear ledge without reading as a card. Density picks the growth
24 /// cap only — the composer starts at one content row regardless.
25 #[must_use]
26 pub fn for_density(density: ComposerDensity, enclosed_panel: bool) -> Self {
27 let border_rows = if enclosed_panel { 2 } else { 1 };
28 let max_total_rows = match density {
29 ComposerDensity::Compact => 7,
30 ComposerDensity::Comfortable => 9,
31 ComposerDensity::Spacious => 12,
32 };
33 Self {
34 border_rows,
35 max_total_rows,
36 }
37 }
38 }
39
40 /// Decide how many rows the composer should occupy.
41 ///
42 /// The height follows the content: one input row when the composer is
43 /// empty or holds a single line, growing one row per content line up to
44 /// the density cap (`max_total_rows`) or the available height, whichever
45 /// is smaller. Menu rows and the border chrome add on top. Compact
46 /// terminals shed the border before they shed typed content.
47 #[must_use]
48 pub fn desired_height(
49 content_lines: usize,
50 extra_menu_lines: usize,
51 available_height: u16,
52 density: ComposerDensity,
53 enclosed_panel: bool,
54 ) -> u16 {
55 let chrome = ComposerChrome::for_density(density, enclosed_panel);
56 let available = available_height.max(1);
57 let content = content_lines.max(1);
58 let wants_panel = enclosed_panel && available >= 3;
59
60 let border = if wants_panel {
61 usize::from(chrome.border_rows)
62 } else if available >= 2 {
63 1
64 } else {
65 0
66 };
67
68 let total = content
69 .saturating_add(extra_menu_lines)
70 .saturating_add(border);
71 let max_height = usize::from(available.min(chrome.max_total_rows).max(1));
72 total.clamp(1, max_height).try_into().unwrap_or(1)
73 }
74
75 /// Top padding inside the content budget. Keep at least one quiet row below a
76 /// short prompt when the budget has room, instead of bottom-pinning
77 /// the caret directly against the phase footer. Compact heights naturally
78 /// report zero padding once the budget collapses.
79 #[must_use]
80 pub fn top_padding(content_lines: usize, rows_budget: usize) -> usize {
81 let content = content_lines.max(1).min(rows_budget.max(1));
82 let spare = rows_budget.saturating_sub(content);
83 spare.saturating_add(1) / 2
84 }
85
86 #[cfg(test)]
87 mod tests {
88 use super::*;
89
90 #[test]
91 fn empty_composer_fits_one_input_row_plus_chrome() {
92 // Auto-fit: an empty/single-line composer takes exactly one input
93 // row plus the quiet-rule border, regardless of density.
94 let height = desired_height(1, 0, 8, ComposerDensity::Comfortable, false);
95 assert_eq!(height, 2, "1 content row + 1 border row");
96 }
97
98 #[test]
99 fn compact_height_sheds_border_before_content() {
100 // Only two rows available: keep a border + one content row.
101 let height = desired_height(1, 0, 2, ComposerDensity::Comfortable, false);
102 assert_eq!(height, 2);
103 }
104
105 #[test]
106 fn content_growth_expands_up_to_the_density_cap() {
107 // Six content rows + border fits under the Comfortable cap of 9.
108 let height = desired_height(6, 0, 12, ComposerDensity::Comfortable, false);
109 assert_eq!(height, 7, "typed content must grow the composer: {height}");
110
111 // Past the cap the density setting wins, not the content.
112 let capped = desired_height(20, 0, 30, ComposerDensity::Comfortable, false);
113 assert_eq!(capped, 9, "Comfortable caps total rows at 9");
114 let spacious = desired_height(20, 0, 30, ComposerDensity::Spacious, false);
115 assert_eq!(spacious, 12, "Spacious caps total rows at 12");
116 }
117
118 #[test]
119 fn single_line_panel_is_one_input_row_plus_both_borders() {
120 let height = desired_height(1, 0, 12, ComposerDensity::Spacious, true);
121 assert_eq!(height, 3, "panel = 2 borders + 1 content row, got {height}");
122 }
123 }
124
124 lines RUST