use rich_rs::{Console, ConsoleOptions, Renderable, Segment, Segments};
use crate::compose::ComposeResult;
use crate::css;
use crate::debug::DebugLayout;
use crate::event::{Event, EventCtx};
use crate::widgets::{
Widget, WidgetStyles,
helpers::{apply_debug_box, fixed_height_from_constraints},
};
pub struct Container {
children: Vec<Box<dyn Widget>>,
children_extracted: bool,
styles: WidgetStyles,
}
impl Container {
pub fn new() -> Self {
Self {
children: Vec::new(),
children_extracted: false,
styles: WidgetStyles::default(),
}
}
pub fn with_child(mut self, child: impl Widget + 'static) -> Self {
self.children.push(Box::new(child));
self
}
pub fn with_compose(mut self, children: ComposeResult) -> Self {
for decl in children {
match decl.builder {
crate::compose::WidgetBuilder::Ready(widget) => self.children.push(widget),
}
}
self
}
pub fn push(&mut self, child: impl Widget + 'static) {
self.children.push(Box::new(child));
}
pub fn children(&self) -> &[Box<dyn Widget>] {
&self.children
}
pub fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
&mut self.children
}
}
impl Widget for Container {
fn has_focus(&self) -> bool {
false
}
fn set_focus(&mut self, _focused: bool) {}
fn compose(&self) -> ComposeResult {
Vec::new()
}
fn take_composed_children(&mut self) -> Vec<Box<dyn Widget>> {
self.children_extracted = true;
std::mem::take(&mut self.children)
}
fn render(&self, _console: &Console, options: &ConsoleOptions) -> Segments {
let width = options.size.0.max(1);
let height_limit = options.size.1.max(1);
let meta = css::selector_meta_generic(self);
let resolved = css::resolve_style(self, &meta);
let paints_surface = resolved.bg.is_some()
|| resolved.hatch.is_some()
|| resolved.border_top.is_set()
|| resolved.border_right.is_set()
|| resolved.border_bottom.is_set()
|| resolved.border_left.is_set()
|| resolved.outline_top.is_set()
|| resolved.outline_right.is_set()
|| resolved.outline_bottom.is_set()
|| resolved.outline_left.is_set();
if !paints_surface {
return Segments::new();
}
let lines = vec![vec![Segment::new(" ".repeat(width))]; height_limit];
let line_count = lines.len();
let mut out = Segments::new();
for (idx, line) in lines.into_iter().enumerate() {
out.extend(line);
if idx + 1 < line_count {
out.push(Segment::line());
}
}
out
}
fn render_with_debug(
&self,
console: &Console,
options: &ConsoleOptions,
debug: &DebugLayout,
) -> Segments {
let rendered = Widget::render(self, console, options);
let width = options.size.0.max(1);
let height_limit = options.size.1.max(1);
let mut lines = Segment::split_and_crop_lines(rendered, width, None, true, false);
lines = Segment::set_shape(&lines, width, Some(height_limit), None, false);
let label = if debug.show_sizes {
Some(format!("{width}x{height_limit}"))
} else {
None
};
let boxed = apply_debug_box(
lines,
width,
height_limit,
label.as_deref(),
debug.style_for(0),
);
let line_count = boxed.len();
let mut out = Segments::new();
for (idx, line) in boxed.into_iter().enumerate() {
out.extend(line);
if idx + 1 < line_count {
out.push(Segment::line());
}
}
out
}
fn on_mount(&mut self) {}
fn on_unmount(&mut self) {}
fn on_tick(&mut self, _tick: u64) {}
fn on_resize(&mut self, _width: u16, _height: u16) {}
fn on_layout(&mut self, _width: u16, _height: u16) {}
fn on_event_capture(&mut self, _event: &Event, _ctx: &mut EventCtx) {}
fn on_event(&mut self, _event: &Event, _ctx: &mut EventCtx) {}
fn on_mouse_move(&mut self, _x: u16, _y: u16) -> bool {
false
}
fn layout_height(&self) -> Option<usize> {
if let Some(fixed) = fixed_height_from_constraints(self.layout_constraints()) {
return Some(fixed);
}
if self.children_extracted {
return None;
}
let mut total = 0usize;
for child in &self.children {
let Some(child_height) = child.layout_height() else {
return None;
};
total = total.saturating_add(child_height.max(1));
}
if total == 0 { None } else { Some(total) }
}
fn content_width(&self) -> Option<usize> {
if self.children_extracted {
return None;
}
let mut max_width = 0usize;
let mut saw = false;
for child in &self.children {
if let Some(width) = child.content_width() {
max_width = max_width.max(width.max(1));
saw = true;
}
}
if saw { Some(max_width.max(1)) } else { None }
}
fn styles(&self) -> Option<&WidgetStyles> {
Some(&self.styles)
}
fn styles_mut(&mut self) -> Option<&mut WidgetStyles> {
Some(&mut self.styles)
}
}
impl Renderable for Container {
fn render(&self, console: &Console, options: &ConsoleOptions) -> Segments {
Widget::render(self, console, options)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::Label;
#[test]
fn compose_returns_empty() {
let c = Container::new().with_child(Label::new("a"));
assert!(c.compose().is_empty());
}
#[test]
fn take_composed_children_extracts_all() {
let mut c = Container::new()
.with_child(Label::new("a"))
.with_child(Label::new("b"));
let children = c.take_composed_children();
assert_eq!(children.len(), 2);
assert!(c.children().is_empty());
}
#[test]
fn extraction_is_idempotent() {
let mut c = Container::new()
.with_child(Label::new("a"))
.with_child(Label::new("b"));
let _ = c.take_composed_children();
assert!(c.take_composed_children().is_empty());
}
#[test]
fn tree_mode_render_transparent_container_is_empty() {
let mut c = Container::new()
.with_child(Label::new("hello"))
.with_child(Label::new("world"));
let _ = c.take_composed_children();
let console = Console::new();
let mut options = console.options().clone();
options.size = (10, 4);
options.max_width = 10;
options.max_height = 4;
let segments = Widget::render(&c, &console, &options);
assert!(segments.is_empty());
}
#[test]
fn tree_mode_render_styled_container_produces_surface_fill() {
let mut c = Container::new()
.with_child(Label::new("hello"))
.with_child(Label::new("world"));
c.styles_mut().unwrap().style.bg = Some(crate::style::Color::rgb(10, 20, 30));
let _ = c.take_composed_children();
let console = Console::new();
let mut options = console.options().clone();
options.size = (10, 4);
options.max_width = 10;
options.max_height = 4;
let segments = Widget::render(&c, &console, &options);
assert!(!segments.is_empty());
}
#[test]
fn tree_mode_on_event_does_not_panic() {
let mut c = Container::new().with_child(Label::new("a"));
let _ = c.take_composed_children();
let mut ctx = EventCtx::default();
c.on_event(&Event::Action(crate::event::Action::FocusNext), &mut ctx);
assert!(!ctx.handled());
}
#[test]
fn tree_mode_on_mouse_move_returns_false() {
let mut c = Container::new().with_child(Label::new("a"));
let _ = c.take_composed_children();
assert!(!c.on_mouse_move(0, 0));
}
#[test]
fn tree_mode_layout_height_returns_none() {
let mut c = Container::new().with_child(Label::new("a"));
let _ = c.take_composed_children();
assert!(c.layout_height().is_none());
}
}