1use crate::tui::Component;
2use crate::tui::component::{RenderCache, RenderCacheKey};
3use crate::tui::overlay::{OverlayEntry, OverlayLayout, OverlayOptions, SizeValue};
4use crate::tui::util::{extract_segments, slice_by_column, visible_width};
5
6const SEGMENT_RESET: &str = "\x1b[0m\x1b]8;;\x07";
8
9struct ChildCache {
11 cache: Option<RenderCache>,
13 dirty: bool,
15}
16
17impl ChildCache {
18 fn new() -> Self {
19 Self {
20 cache: None,
21 dirty: true,
22 }
23 }
24}
25
26pub struct Container {
29 children: Vec<Box<dyn Component>>,
30 child_caches: Vec<ChildCache>,
32 overlay_stack: Vec<OverlayEntry>,
34 term_height: usize,
36}
37
38impl Container {
39 pub fn new() -> Self {
40 Self {
41 children: Vec::new(),
42 child_caches: Vec::new(),
43 overlay_stack: Vec::new(),
44 term_height: 24,
45 }
46 }
47
48 pub fn set_term_height(&mut self, height: usize) {
50 self.term_height = height;
51 }
52
53 pub fn add_child(&mut self, component: Box<dyn Component>) {
56 self.child_caches.push(ChildCache::new());
57 self.children.push(component);
58 }
59
60 pub fn remove_child(&mut self, component: &dyn Component) {
61 let idx = self.children.iter().position(|c| {
62 std::ptr::eq(
63 c.as_ref() as *const dyn Component,
64 component as *const dyn Component,
65 )
66 });
67 if let Some(idx) = idx {
68 self.children.remove(idx);
69 self.child_caches.remove(idx);
70 }
71 }
72
73 pub fn clear(&mut self) {
74 self.children.clear();
75 self.child_caches.clear();
76 }
77
78 pub fn children(&self) -> &[Box<dyn Component>] {
79 &self.children
80 }
81
82 pub fn children_mut(&mut self) -> &mut [Box<dyn Component>] {
83 &mut self.children
84 }
85
86 pub fn invalidate_all(&mut self) {
88 for cache in &mut self.child_caches {
89 cache.dirty = true;
90 cache.cache = None;
91 }
92 }
93
94 pub fn invalidate_child(&mut self, index: usize) {
96 if let Some(cache) = self.child_caches.get_mut(index) {
97 cache.dirty = true;
98 cache.cache = None;
99 }
100 }
101
102 pub fn len(&self) -> usize {
104 self.children.len()
105 }
106
107 pub fn pop_child(&mut self) -> Option<Box<dyn Component>> {
109 self.child_caches.pop();
110 self.children.pop()
111 }
112
113 pub fn is_empty(&self) -> bool {
115 self.children.is_empty()
116 }
117
118 pub fn last_child(&self) -> Option<&dyn Component> {
120 self.children.last().map(|c| c.as_ref())
121 }
122
123 pub fn show_overlay(&mut self, component: Box<dyn Component>, options: OverlayOptions) -> u64 {
127 let id = self.overlay_stack.len() as u64;
128 self.overlay_stack.push(OverlayEntry {
129 component,
130 options,
131 hidden: false,
132 focus_order: id,
133 id,
134 });
135 id
136 }
137
138 pub fn hide_overlay(&mut self, id: u64) {
140 self.overlay_stack.retain(|e| e.id != id);
141 }
142
143 pub fn pop_overlay(&mut self) {
145 self.overlay_stack.pop();
146 }
147
148 pub fn has_overlays(&self) -> bool {
150 self.overlay_stack.iter().any(|e| !e.hidden)
151 }
152
153 pub fn clear_overlays(&mut self) {
155 self.overlay_stack.clear();
156 }
157
158 pub fn overlay_stack(&self) -> &[OverlayEntry] {
160 &self.overlay_stack
161 }
162
163 pub fn overlay_stack_mut(&mut self) -> &mut Vec<OverlayEntry> {
164 &mut self.overlay_stack
165 }
166}
167
168impl Default for Container {
169 fn default() -> Self {
170 Self::new()
171 }
172}
173
174impl Container {
175 fn composite_overlays(
177 &mut self,
178 base_lines: &[String],
179 term_width: usize,
180 term_height: usize,
181 ) -> Vec<String> {
182 let mut result = base_lines.to_vec();
183
184 let mut indices: Vec<usize> = self
186 .overlay_stack
187 .iter()
188 .enumerate()
189 .filter(|(_, e)| !e.hidden)
190 .map(|(i, _)| i)
191 .collect();
192 indices.sort_by_key(|&i| self.overlay_stack[i].focus_order);
193
194 let mut min_lines_needed = result.len();
195
196 struct RenderedOverlay {
197 overlay_lines: Vec<String>,
198 layout: OverlayLayout,
199 }
200
201 let mut rendered: Vec<RenderedOverlay> = Vec::new();
202 for &idx in &indices {
203 let options = self.overlay_stack[idx].options.clone();
204 let layout = self.resolve_overlay_layout(&options, 0, term_width, term_height);
205
206 let mut overlay_lines = self.overlay_stack[idx].component.render(layout.width);
207
208 let overlay_height = if let Some(max_h) = layout.max_height {
209 overlay_lines.truncate(max_h);
210 overlay_lines.len()
211 } else {
212 overlay_lines.len()
213 };
214
215 let layout =
216 self.resolve_overlay_layout(&options, overlay_height, term_width, term_height);
217
218 min_lines_needed = min_lines_needed.max(layout.row + overlay_lines.len());
219
220 rendered.push(RenderedOverlay {
221 overlay_lines,
222 layout,
223 });
224 }
225
226 let working_height = result.len().max(term_height).max(min_lines_needed);
227 while result.len() < working_height {
228 result.push(String::new());
229 }
230
231 let viewport_start = working_height.saturating_sub(term_height);
232
233 for ro in &rendered {
234 for (i, overlay_line) in ro.overlay_lines.iter().enumerate() {
235 let idx = viewport_start + ro.layout.row + i;
236 if idx < result.len() {
237 let truncated = if visible_width(overlay_line) > ro.layout.width {
238 slice_by_column(overlay_line, 0, ro.layout.width)
239 } else {
240 overlay_line.clone()
241 };
242 result[idx] = self.composite_line_at(
243 &result[idx],
244 &truncated,
245 ro.layout.col,
246 ro.layout.width,
247 term_width,
248 );
249 }
250 }
251 }
252
253 result
254 }
255
256 fn composite_line_at(
258 &self,
259 base_line: &str,
260 overlay_line: &str,
261 start_col: usize,
262 overlay_width: usize,
263 total_width: usize,
264 ) -> String {
265 let after_start = start_col + overlay_width;
266
267 let (before, before_width, after, after_width) = extract_segments(
268 base_line,
269 start_col,
270 after_start,
271 total_width.saturating_sub(after_start),
272 true,
273 );
274
275 let overlay = slice_by_column(overlay_line, 0, overlay_width);
276 let overlay_vis = visible_width(&overlay);
277
278 let before_pad = start_col.saturating_sub(before_width);
279 let overlay_pad = overlay_width.saturating_sub(overlay_vis);
280 let actual_before_width = before_width.max(start_col);
281 let actual_overlay_width = overlay_vis.max(overlay_width);
282 let after_target = total_width.saturating_sub(actual_before_width + actual_overlay_width);
283 let after_pad = after_target.saturating_sub(after_width);
284
285 let mut result = String::new();
286 result.push_str(&before);
287 result.push_str(&" ".repeat(before_pad));
288 result.push_str(SEGMENT_RESET);
289 result.push_str(&overlay);
290 result.push_str(&" ".repeat(overlay_pad));
291 result.push_str(SEGMENT_RESET);
292 result.push_str(&after);
293 result.push_str(&" ".repeat(after_pad));
294
295 let rw = visible_width(&result);
296 if rw > total_width {
297 result = slice_by_column(&result, 0, total_width);
298 }
299
300 result
301 }
302
303 fn resolve_overlay_layout(
305 &self,
306 options: &OverlayOptions,
307 overlay_height: usize,
308 term_width: usize,
309 term_height: usize,
310 ) -> OverlayLayout {
311 let margin = options.margin.unwrap_or_default();
312 let margin_top = margin.top;
313 let margin_right = margin.right;
314 let margin_bottom = margin.bottom;
315 let margin_left = margin.left;
316
317 let avail_width = (term_width - margin_left - margin_right).max(1);
318 let avail_height = (term_height - margin_top - margin_bottom).max(1);
319
320 let width = options
321 .width
322 .map(|sv| sv.resolve(term_width))
323 .unwrap_or_else(|| 80.min(avail_width));
324 let width = options.min_width.map(|mw| width.max(mw)).unwrap_or(width);
325 let width = width.max(1).min(avail_width);
326
327 let max_height = options.max_height.map(|sv| sv.resolve(term_height));
328 let max_height = max_height.map(|mh| mh.max(1).min(avail_height));
329
330 let effective_height = match max_height {
331 Some(mh) => overlay_height.min(mh),
332 None => overlay_height,
333 };
334
335 let row = if let Some(ref row_sv) = options.row {
336 match row_sv {
337 SizeValue::Absolute(r) => *r,
338 SizeValue::Percent(p) => {
339 let max_row = avail_height - effective_height;
340 margin_top + ((max_row as f64 * p / 100.0).floor() as usize)
341 }
342 }
343 } else {
344 let anchor = options.anchor.unwrap_or_default();
345 Self::resolve_anchor_row(anchor, effective_height, avail_height, margin_top)
346 };
347
348 let col = if let Some(ref col_sv) = options.col {
349 match col_sv {
350 SizeValue::Absolute(c) => *c,
351 SizeValue::Percent(p) => {
352 let max_col = avail_width - width;
353 margin_left + ((max_col as f64 * p / 100.0).floor() as usize)
354 }
355 }
356 } else {
357 let anchor = options.anchor.unwrap_or_default();
358 Self::resolve_anchor_col(anchor, width, avail_width, margin_left)
359 };
360
361 let row = (row as isize + options.offset_y.unwrap_or(0)) as usize;
362 let col = (col as isize + options.offset_x.unwrap_or(0)) as usize;
363
364 OverlayLayout {
365 width,
366 row,
367 col,
368 max_height,
369 }
370 }
371
372 fn resolve_anchor_row(
373 anchor: crate::tui::overlay::OverlayAnchor,
374 overlay_height: usize,
375 avail_height: usize,
376 margin_top: usize,
377 ) -> usize {
378 use crate::tui::overlay::OverlayAnchor::*;
379 match anchor {
380 Center | LeftCenter | RightCenter => {
381 margin_top + (avail_height.saturating_sub(overlay_height) / 2)
382 }
383 TopLeft | TopCenter | TopRight => margin_top,
384 BottomLeft | BottomCenter | BottomRight => {
385 margin_top + avail_height.saturating_sub(overlay_height)
386 }
387 }
388 }
389
390 fn resolve_anchor_col(
391 anchor: crate::tui::overlay::OverlayAnchor,
392 overlay_width: usize,
393 avail_width: usize,
394 margin_left: usize,
395 ) -> usize {
396 use crate::tui::overlay::OverlayAnchor::*;
397 match anchor {
398 Center | TopCenter | BottomCenter => {
399 margin_left + (avail_width.saturating_sub(overlay_width) / 2)
400 }
401 TopLeft | LeftCenter | BottomLeft => margin_left,
402 TopRight | RightCenter | BottomRight => {
403 margin_left + avail_width.saturating_sub(overlay_width)
404 }
405 }
406 }
407}
408
409impl Component for Container {
410 fn render(&mut self, width: usize) -> Vec<String> {
411 let mut lines = Vec::new();
412 for (idx, child) in self.children.iter_mut().enumerate() {
413 let cache = &mut self.child_caches[idx];
414 if !cache.dirty
417 && let Some(ref cached) = cache.cache
418 && cached.key.width == width
419 && child.cache_key(width).is_some_and(|k| k == cached.key)
420 {
421 lines.extend(cached.lines.clone());
422 continue;
423 }
424 let child_lines = child.render(width);
425 child.clear_dirty();
426 let cache_key = child.cache_key(width).unwrap_or(RenderCacheKey {
427 width,
428 expanded: false,
429 state_hash: 0,
430 });
431 cache.cache = Some(RenderCache {
432 key: cache_key,
433 lines: child_lines.clone(),
434 });
435 cache.dirty = false;
436 lines.extend(child_lines);
437 }
438 if !self.overlay_stack.is_empty() {
440 lines = self.composite_overlays(&lines, width, self.term_height);
441 }
442 lines
443 }
444
445 fn handle_input(&mut self, key: &crossterm::event::KeyEvent) -> bool {
446 for entry in self.overlay_stack.iter_mut().rev() {
448 if !entry.hidden && entry.component.handle_input(key) {
449 return true;
450 }
451 }
452 for child in self.children.iter_mut().rev() {
454 if child.handle_input(key) {
455 return true;
456 }
457 }
458 false
459 }
460
461 fn invalidate(&mut self) {
462 for child in &mut self.children {
463 child.invalidate();
464 }
465 for cache in &mut self.child_caches {
466 cache.dirty = true;
467 cache.cache = None;
468 }
469 }
470
471 fn is_dirty(&self) -> bool {
472 self.child_caches.iter().any(|c| c.dirty)
473 }
474
475 fn clear_dirty(&mut self) {
476 for cache in &mut self.child_caches {
477 cache.dirty = false;
478 }
479 }
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485 use crate::tui::component::Component;
486
487 struct TrackRender {
490 render_count: usize,
491 dirty: bool,
492 label: String,
493 }
494
495 impl TrackRender {
496 fn new(label: &str) -> Self {
497 Self {
498 render_count: 0,
499 dirty: true,
500 label: label.to_string(),
501 }
502 }
503 }
504
505 impl Component for TrackRender {
506 fn render(&mut self, _width: usize) -> Vec<String> {
507 self.render_count += 1;
508 vec![format!("{}[{}]", self.label, self.render_count)]
509 }
510
511 fn cache_key(&self, width: usize) -> Option<RenderCacheKey> {
512 Some(RenderCacheKey {
513 width,
514 expanded: false,
515 state_hash: self.render_count as u64,
516 })
517 }
518
519 fn is_dirty(&self) -> bool {
520 self.dirty
521 }
522
523 fn clear_dirty(&mut self) {
524 self.dirty = false;
525 }
526 }
527
528 #[test]
529 fn test_re_render_when_dirty() {
530 let mut c = Container::new();
531 let child = Box::new(TrackRender::new("a"));
532 c.add_child(child);
533
534 let lines = c.render(80);
536 assert_eq!(lines.len(), 1);
537 assert_eq!(lines[0], "a[1]");
538
539 let lines = c.render(80);
542 assert_eq!(lines[0], "a[1]"); c.invalidate_all();
546 let lines = c.render(80);
547 assert_eq!(lines[0], "a[2]"); }
549
550 #[test]
551 fn test_re_render_when_child_stays_dirty() {
552 struct AlwaysDirty;
555
556 impl Component for AlwaysDirty {
557 fn render(&mut self, _width: usize) -> Vec<String> {
558 vec!["fresh".to_string()]
559 }
560
561 fn is_dirty(&self) -> bool {
562 true
563 }
564 }
565
566 let mut c = Container::new();
567 c.add_child(Box::new(AlwaysDirty));
568
569 let lines1 = c.render(80);
570 assert_eq!(lines1[0], "fresh");
571
572 let lines2 = c.render(80);
574 assert_eq!(lines2[0], "fresh");
575
576 assert!(
580 !c.child_caches[0].dirty,
581 "child cache should be marked clean after render"
582 );
583 }
584
585 #[test]
586 fn test_cached_after_non_dirty_render() {
587 let mut c = Container::new();
588 c.add_child(Box::new(TrackRender::new("x")));
589
590 c.render(80);
592
593 let lines = c.render(40);
595 assert_eq!(lines[0], "x[2]"); }
597
598 #[test]
599 fn test_mixed_dirty_and_not_dirty_children() {
600 struct SometimesDirty {
601 toggle: bool,
602 }
603 impl Component for SometimesDirty {
604 fn render(&mut self, _width: usize) -> Vec<String> {
605 vec!["s".to_string()]
606 }
607 fn is_dirty(&self) -> bool {
608 self.toggle
609 }
610 fn clear_dirty(&mut self) {
611 }
614 }
615
616 let mut c = Container::new();
617 c.add_child(Box::new(TrackRender::new("a")));
618 c.add_child(Box::new(SometimesDirty { toggle: false }));
619
620 let lines = c.render(80);
622 assert_eq!(lines[0], "a[1]");
623 assert_eq!(lines[1], "s");
624
625 let lines = c.render(80);
628 assert_eq!(lines[0], "a[1]"); assert_eq!(lines[1], "s");
630 }
631}