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(
178 &mut self,
179 base_lines: &[String],
180 term_width: usize,
181 term_height: usize,
182 ) -> Vec<String> {
183 if self.overlay_stack.is_empty() {
184 return base_lines.to_vec();
185 }
186 let mut result = base_lines.to_vec();
187
188 let mut indices: Vec<usize> = self
190 .overlay_stack
191 .iter()
192 .enumerate()
193 .filter(|(_, e)| !e.hidden)
194 .map(|(i, _)| i)
195 .collect();
196 indices.sort_by_key(|&i| self.overlay_stack[i].focus_order);
197
198 struct RenderedOverlay {
200 overlay_lines: Vec<String>,
201 row: usize,
202 col: usize,
203 w: usize,
204 }
205
206 let mut rendered: Vec<RenderedOverlay> = Vec::new();
207 let mut min_lines_needed = result.len();
208
209 for &idx in &indices {
210 let options = self.overlay_stack[idx].options.clone();
211
212 let first_layout = self.resolve_overlay_layout(&options, 0, term_width, term_height);
215 let width = first_layout.width;
216 let max_height = first_layout.max_height;
217
218 let mut overlay_lines = self.overlay_stack[idx].component.render(width);
220
221 if let Some(mh) = max_height {
223 overlay_lines.truncate(mh);
224 }
225
226 let overlay_len = overlay_lines.len();
227
228 let layout =
230 self.resolve_overlay_layout(&options, overlay_len, term_width, term_height);
231
232 min_lines_needed = min_lines_needed.max(layout.row + overlay_len);
233
234 rendered.push(RenderedOverlay {
235 overlay_lines,
236 row: layout.row,
237 col: layout.col,
238 w: width,
239 });
240 }
241
242 let working_height = result.len().max(term_height).max(min_lines_needed);
246
247 while result.len() < working_height {
249 result.push(String::new());
250 }
251
252 let viewport_start = working_height.saturating_sub(term_height);
253
254 for ro in &rendered {
256 for (i, overlay_line) in ro.overlay_lines.iter().enumerate() {
257 let idx = viewport_start + ro.row + i;
258 if idx < result.len() {
259 let truncated = if visible_width(overlay_line) > ro.w {
261 slice_by_column(overlay_line, 0, ro.w)
262 } else {
263 overlay_line.clone()
264 };
265 result[idx] =
266 self.composite_line_at(&result[idx], &truncated, ro.col, ro.w, term_width);
267 }
268 }
269 }
270
271 result
272 }
273
274 fn composite_line_at(
276 &self,
277 base_line: &str,
278 overlay_line: &str,
279 start_col: usize,
280 overlay_width: usize,
281 total_width: usize,
282 ) -> String {
283 let after_start = start_col + overlay_width;
284
285 let (before, before_width, after, after_width) = extract_segments(
286 base_line,
287 start_col,
288 after_start,
289 total_width.saturating_sub(after_start),
290 true,
291 );
292
293 let overlay = slice_by_column(overlay_line, 0, overlay_width);
294 let overlay_vis = visible_width(&overlay);
295
296 let before_pad = start_col.saturating_sub(before_width);
297 let overlay_pad = overlay_width.saturating_sub(overlay_vis);
298 let actual_before_width = before_width.max(start_col);
299 let actual_overlay_width = overlay_vis.max(overlay_width);
300 let after_target = total_width.saturating_sub(actual_before_width + actual_overlay_width);
301 let after_pad = after_target.saturating_sub(after_width);
302
303 let mut result = String::new();
304 result.push_str(&before);
305 result.push_str(&" ".repeat(before_pad));
306 result.push_str(SEGMENT_RESET);
307 result.push_str(&overlay);
308 result.push_str(&" ".repeat(overlay_pad));
309 result.push_str(SEGMENT_RESET);
310 result.push_str(&after);
311 result.push_str(&" ".repeat(after_pad));
312
313 let rw = visible_width(&result);
314 if rw > total_width {
315 result = slice_by_column(&result, 0, total_width);
316 }
317
318 result
319 }
320
321 fn resolve_overlay_layout(
323 &self,
324 options: &OverlayOptions,
325 overlay_height: usize,
326 term_width: usize,
327 term_height: usize,
328 ) -> OverlayLayout {
329 let margin = options.margin.unwrap_or_default();
330 let margin_top = margin.top;
331 let margin_right = margin.right;
332 let margin_bottom = margin.bottom;
333 let margin_left = margin.left;
334
335 let avail_width = (term_width - margin_left - margin_right).max(1);
336 let avail_height = (term_height - margin_top - margin_bottom).max(1);
337
338 let width = options
339 .width
340 .map(|sv| sv.resolve(term_width))
341 .unwrap_or_else(|| 80.min(avail_width));
342 let width = options.min_width.map(|mw| width.max(mw)).unwrap_or(width);
343 let width = width.max(1).min(avail_width);
344
345 let max_height = options.max_height.map(|sv| sv.resolve(term_height));
346 let max_height = max_height.map(|mh| mh.max(1).min(avail_height));
347
348 let effective_height = match max_height {
349 Some(mh) => overlay_height.min(mh),
350 None => overlay_height,
351 };
352
353 let row = if let Some(ref row_sv) = options.row {
354 match row_sv {
355 SizeValue::Absolute(r) => *r,
356 SizeValue::Percent(p) => {
357 let max_row = avail_height - effective_height;
358 margin_top + ((max_row as f64 * p / 100.0).floor() as usize)
359 }
360 }
361 } else {
362 let anchor = options.anchor.unwrap_or_default();
363 Self::resolve_anchor_row(anchor, effective_height, avail_height, margin_top)
364 };
365
366 let col = if let Some(ref col_sv) = options.col {
367 match col_sv {
368 SizeValue::Absolute(c) => *c,
369 SizeValue::Percent(p) => {
370 let max_col = avail_width - width;
371 margin_left + ((max_col as f64 * p / 100.0).floor() as usize)
372 }
373 }
374 } else {
375 let anchor = options.anchor.unwrap_or_default();
376 Self::resolve_anchor_col(anchor, width, avail_width, margin_left)
377 };
378
379 let row = (row as isize + options.offset_y.unwrap_or(0)) as usize;
380 let col = (col as isize + options.offset_x.unwrap_or(0)) as usize;
381
382 OverlayLayout {
383 width,
384 row,
385 col,
386 max_height,
387 }
388 }
389
390 fn resolve_anchor_row(
391 anchor: crate::tui::overlay::OverlayAnchor,
392 overlay_height: usize,
393 avail_height: usize,
394 margin_top: usize,
395 ) -> usize {
396 use crate::tui::overlay::OverlayAnchor::*;
397 match anchor {
398 Center | LeftCenter | RightCenter => {
399 margin_top + (avail_height.saturating_sub(overlay_height) / 2)
400 }
401 TopLeft | TopCenter | TopRight => margin_top,
402 BottomLeft | BottomCenter | BottomRight => {
403 margin_top + avail_height.saturating_sub(overlay_height)
404 }
405 }
406 }
407
408 fn resolve_anchor_col(
409 anchor: crate::tui::overlay::OverlayAnchor,
410 overlay_width: usize,
411 avail_width: usize,
412 margin_left: usize,
413 ) -> usize {
414 use crate::tui::overlay::OverlayAnchor::*;
415 match anchor {
416 Center | TopCenter | BottomCenter => {
417 margin_left + (avail_width.saturating_sub(overlay_width) / 2)
418 }
419 TopLeft | LeftCenter | BottomLeft => margin_left,
420 TopRight | RightCenter | BottomRight => {
421 margin_left + avail_width.saturating_sub(overlay_width)
422 }
423 }
424 }
425}
426
427impl Component for Container {
428 fn render(&mut self, width: usize) -> Vec<String> {
429 let mut lines = Vec::new();
430 for (idx, child) in self.children.iter_mut().enumerate() {
431 let cache = &mut self.child_caches[idx];
432 if !cache.dirty
435 && let Some(ref cached) = cache.cache
436 && cached.key.width == width
437 && child.cache_key(width).is_some_and(|k| k == cached.key)
438 {
439 lines.extend(cached.lines.clone());
440 continue;
441 }
442 let child_lines = child.render(width);
443 child.clear_dirty();
444 let cache_key = child.cache_key(width).unwrap_or(RenderCacheKey {
445 width,
446 expanded: false,
447 state_hash: 0,
448 });
449 cache.cache = Some(RenderCache {
450 key: cache_key,
451 lines: child_lines.clone(),
452 });
453 cache.dirty = false;
454 lines.extend(child_lines);
455 }
456 if !self.overlay_stack.is_empty() {
458 lines = self.composite_overlays(&lines, width, self.term_height);
459 }
460 lines
461 }
462
463 fn handle_input(&mut self, key: &crossterm::event::KeyEvent) -> bool {
464 for entry in self.overlay_stack.iter_mut().rev() {
466 if !entry.hidden && entry.component.handle_input(key) {
467 return true;
468 }
469 }
470 for child in self.children.iter_mut().rev() {
472 if child.handle_input(key) {
473 return true;
474 }
475 }
476 false
477 }
478
479 fn invalidate(&mut self) {
480 for child in &mut self.children {
481 child.invalidate();
482 }
483 for cache in &mut self.child_caches {
484 cache.dirty = true;
485 cache.cache = None;
486 }
487 }
488
489 fn is_dirty(&self) -> bool {
490 self.child_caches.iter().any(|c| c.dirty)
491 }
492
493 fn clear_dirty(&mut self) {
494 for cache in &mut self.child_caches {
495 cache.dirty = false;
496 }
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503 use crate::tui::component::Component;
504
505 struct TrackRender {
508 render_count: usize,
509 dirty: bool,
510 label: String,
511 }
512
513 impl TrackRender {
514 fn new(label: &str) -> Self {
515 Self {
516 render_count: 0,
517 dirty: true,
518 label: label.to_string(),
519 }
520 }
521 }
522
523 impl Component for TrackRender {
524 fn render(&mut self, _width: usize) -> Vec<String> {
525 self.render_count += 1;
526 vec![format!("{}[{}]", self.label, self.render_count)]
527 }
528
529 fn cache_key(&self, width: usize) -> Option<RenderCacheKey> {
530 Some(RenderCacheKey {
531 width,
532 expanded: false,
533 state_hash: self.render_count as u64,
534 })
535 }
536
537 fn is_dirty(&self) -> bool {
538 self.dirty
539 }
540
541 fn clear_dirty(&mut self) {
542 self.dirty = false;
543 }
544 }
545
546 #[test]
547 fn test_re_render_when_dirty() {
548 let mut c = Container::new();
549 let child = Box::new(TrackRender::new("a"));
550 c.add_child(child);
551
552 let lines = c.render(80);
554 assert_eq!(lines.len(), 1);
555 assert_eq!(lines[0], "a[1]");
556
557 let lines = c.render(80);
560 assert_eq!(lines[0], "a[1]"); c.invalidate_all();
564 let lines = c.render(80);
565 assert_eq!(lines[0], "a[2]"); }
567
568 #[test]
569 fn test_re_render_when_child_stays_dirty() {
570 struct AlwaysDirty;
573
574 impl Component for AlwaysDirty {
575 fn render(&mut self, _width: usize) -> Vec<String> {
576 vec!["fresh".to_string()]
577 }
578
579 fn is_dirty(&self) -> bool {
580 true
581 }
582 }
583
584 let mut c = Container::new();
585 c.add_child(Box::new(AlwaysDirty));
586
587 let lines1 = c.render(80);
588 assert_eq!(lines1[0], "fresh");
589
590 let lines2 = c.render(80);
592 assert_eq!(lines2[0], "fresh");
593
594 assert!(
598 !c.child_caches[0].dirty,
599 "child cache should be marked clean after render"
600 );
601 }
602
603 #[test]
604 fn test_cached_after_non_dirty_render() {
605 let mut c = Container::new();
606 c.add_child(Box::new(TrackRender::new("x")));
607
608 c.render(80);
610
611 let lines = c.render(40);
613 assert_eq!(lines[0], "x[2]"); }
615
616 #[test]
617 fn test_mixed_dirty_and_not_dirty_children() {
618 struct SometimesDirty {
619 toggle: bool,
620 }
621 impl Component for SometimesDirty {
622 fn render(&mut self, _width: usize) -> Vec<String> {
623 vec!["s".to_string()]
624 }
625 fn is_dirty(&self) -> bool {
626 self.toggle
627 }
628 fn clear_dirty(&mut self) {
629 }
632 }
633
634 let mut c = Container::new();
635 c.add_child(Box::new(TrackRender::new("a")));
636 c.add_child(Box::new(SometimesDirty { toggle: false }));
637
638 let lines = c.render(80);
640 assert_eq!(lines[0], "a[1]");
641 assert_eq!(lines[1], "s");
642
643 let lines = c.render(80);
646 assert_eq!(lines[0], "a[1]"); assert_eq!(lines[1], "s");
648 }
649}