1use std::any::Any;
6use std::sync::{Arc, Mutex};
7
8use super::component::Component;
9use super::layout_node::{HStackNode, LayoutNode, LayoutNodeProvider, StackLayoutEntry};
10use super::vstack::{StackAlign, StackEntry, StackEntryOptions, StackOptions};
11use crate::ansi::{slice_by_column, visible_width};
12
13#[derive(Clone)]
17pub struct HStack {
18 children: Arc<Mutex<Vec<StackEntry>>>,
19 options: StackOptions,
20}
21
22impl HStack {
23 pub fn new() -> Self {
25 Self {
26 children: Arc::new(Mutex::new(Vec::new())),
27 options: StackOptions::default(),
28 }
29 }
30
31 pub fn with_options(options: StackOptions) -> Self {
33 Self {
34 children: Arc::new(Mutex::new(Vec::new())),
35 options,
36 }
37 }
38
39 pub fn from_entries(children: Vec<StackEntry>) -> Self {
41 Self {
42 children: Arc::new(Mutex::new(children)),
43 options: StackOptions::default(),
44 }
45 }
46
47 pub fn add_child(&self, component: Arc<dyn Component>) {
49 if let Ok(mut children) = self.children.lock() {
50 children.push(StackEntry::new(component));
51 }
52 }
53
54 pub fn add_child_with_options(
56 &self,
57 component: Arc<dyn Component>,
58 options: StackEntryOptions,
59 ) {
60 if let Ok(mut children) = self.children.lock() {
61 children.push(StackEntry::with_options(component, options));
62 }
63 }
64
65 pub fn remove_child(&self, component: &Arc<dyn Component>) {
67 if let Ok(mut children) = self.children.lock() {
68 children.retain(|e| !Arc::ptr_eq(&e.component, component));
69 }
70 }
71
72 pub fn clear(&self) {
74 if let Ok(mut children) = self.children.lock() {
75 children.clear();
76 }
77 }
78
79 pub fn gap(&self) -> usize {
81 self.options.gap
82 }
83
84 pub fn align(&self) -> StackAlign {
86 self.options.align
87 }
88
89 pub fn get_children(&self) -> Vec<StackEntry> {
91 self.children.lock().map(|c| c.clone()).unwrap_or_default()
92 }
93
94 pub fn get_layout_entries(&self) -> Vec<StackLayoutEntry> {
96 self.children
97 .lock()
98 .map(|c| c.iter().map(|e| e.to_layout_entry()).collect())
99 .unwrap_or_default()
100 }
101}
102
103impl Default for HStack {
104 fn default() -> Self {
105 Self::new()
106 }
107}
108
109impl Component for HStack {
110 fn render(&self, width: usize) -> Vec<String> {
111 let children = self.children.lock().map(|c| c.clone()).unwrap_or_default();
112 if children.is_empty() {
113 return vec![String::new()];
114 }
115
116 let mut child_data: Vec<(Vec<String>, usize)> = Vec::new();
118 let mut total_basis = 0usize;
119 let mut total_grow = 0usize;
120 let mut grow_indices = Vec::new();
121
122 for (i, entry) in children.iter().enumerate() {
123 let lines = entry.component.render(width);
125 let max_line_width = lines.iter().map(|l| visible_width(l)).max().unwrap_or(0);
126
127 let basis = entry.options.basis.unwrap_or(max_line_width);
128 let min_size = entry.options.min_size;
129 let max_size = entry.options.max_size.unwrap_or(width);
130
131 let basis = basis.clamp(min_size, max_size);
132
133 child_data.push((lines, basis));
134 total_basis += basis;
135
136 if entry.options.grow > 0 {
137 total_grow += entry.options.grow;
138 grow_indices.push(i);
139 }
140 }
141
142 let total_gap = if children.len() > 1 {
144 self.options.gap * (children.len() - 1)
145 } else {
146 0
147 };
148 total_basis += total_gap;
149
150 let available = width;
152 if total_basis < available && total_grow > 0 {
153 let remaining = available - total_basis;
154 for &i in &grow_indices {
155 let entry = &children[i];
156 let share = (remaining * entry.options.grow) / total_grow;
157 let max_size = entry.options.max_size.unwrap_or(width);
158 let current = child_data[i].1;
159 let new_width = (current + share).min(max_size);
160 child_data[i].1 = new_width;
161 }
162 }
163
164 let max_height = child_data
166 .iter()
167 .map(|(lines, _)| lines.len())
168 .max()
169 .unwrap_or(1);
170
171 let mut result: Vec<String> = vec![String::new(); max_height];
173
174 for (line_idx, result_line) in result.iter_mut().enumerate() {
175 let mut current_col = 0;
176
177 for (child_idx, (lines, child_width)) in child_data.iter().enumerate() {
178 if child_idx > 0 && self.options.gap > 0 {
180 result_line.push_str(&" ".repeat(self.options.gap));
181 current_col += self.options.gap;
182 }
183
184 let child_line = if line_idx < lines.len() {
186 &lines[line_idx]
187 } else {
188 ""
189 };
190
191 let line_width = visible_width(child_line);
193 if line_width < *child_width {
194 match self.options.align {
196 StackAlign::Stretch | StackAlign::Start => {
197 result_line.push_str(child_line);
198 result_line.push_str(&" ".repeat(child_width - line_width));
199 }
200 StackAlign::Center => {
201 let left_pad = (child_width - line_width) / 2;
202 let right_pad = child_width - line_width - left_pad;
203 result_line.push_str(&" ".repeat(left_pad));
204 result_line.push_str(child_line);
205 result_line.push_str(&" ".repeat(right_pad));
206 }
207 StackAlign::End => {
208 result_line.push_str(&" ".repeat(child_width - line_width));
209 result_line.push_str(child_line);
210 }
211 }
212 } else if line_width > *child_width {
213 let truncated = slice_by_column(child_line, 0, *child_width, true);
215 result_line.push_str(&truncated);
216 } else {
217 result_line.push_str(child_line);
218 }
219
220 current_col += child_width;
221 }
222
223 if visible_width(result_line) > width {
225 *result_line = slice_by_column(result_line, 0, width, true);
226 }
227 }
228
229 result
230 }
231
232 fn invalidate(&self) {
233 if let Ok(children) = self.children.lock() {
234 for entry in children.iter() {
235 entry.component.invalidate();
236 }
237 }
238 }
239
240 fn as_any(&self) -> &dyn Any {
241 self
242 }
243}
244
245impl LayoutNodeProvider for HStack {
246 fn layout_node(&self) -> Option<LayoutNode> {
247 Some(LayoutNode::HStack(HStackNode {
248 entries: self.get_layout_entries(),
249 gap: self.options.gap,
250 align: self.options.align,
251 }))
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use crate::Text;
259
260 #[test]
261 fn test_hstack_horizontal_layout() {
262 let hstack = HStack::new();
263 hstack.add_child(Arc::new(Text::new("Hello", 0, 0)));
264 hstack.add_child(Arc::new(Text::new("World", 0, 0)));
265
266 let lines = hstack.render(20);
267 assert_eq!(lines.len(), 1);
268 assert!(lines[0].contains("Hello"));
269 assert!(lines[0].contains("World"));
270 }
271}