use rich_rs::{Console, ConsoleOptions, Renderable, Segments};
use crate::debug::DebugLayout;
use crate::event::{Event, EventCtx};
use crate::message::MessageEvent;
use crate::widgets::{
LayoutConstraints, Spacer, Widget, WidgetStyles,
helpers::{clamp_with_constraints, merge_constraints},
};
pub struct Constrained {
child: Box<dyn Widget>,
constraints: LayoutConstraints,
styles: WidgetStyles,
child_extracted: bool,
extracted_child_layout_height: Option<usize>,
extracted_child_content_width: Option<usize>,
}
impl Constrained {
pub fn new(child: impl Widget + 'static) -> Self {
Self {
child: Box::new(child),
constraints: LayoutConstraints::default(),
styles: WidgetStyles::default(),
child_extracted: false,
extracted_child_layout_height: None,
extracted_child_content_width: None,
}
}
pub fn min_width(mut self, value: usize) -> Self {
self.constraints = self.constraints.min_width(value);
self
}
pub fn max_width(mut self, value: usize) -> Self {
self.constraints = self.constraints.max_width(value);
self
}
pub fn min_height(mut self, value: usize) -> Self {
self.constraints = self.constraints.min_height(value);
self
}
pub fn max_height(mut self, value: usize) -> Self {
self.constraints = self.constraints.max_height(value);
self
}
}
impl Widget for Constrained {
fn clips_descendants_to_content(&self) -> bool {
true
}
fn take_composed_children(&mut self) -> Vec<Box<dyn Widget>> {
if self.child_extracted {
return Vec::new();
}
self.child_extracted = true;
self.extracted_child_layout_height = self.child.layout_height();
self.extracted_child_content_width = self.child.content_width();
let child = std::mem::replace(&mut self.child, Box::new(Spacer::new(1)));
vec![child]
}
fn render(&self, console: &Console, options: &ConsoleOptions) -> Segments {
if self.child_extracted {
return Segments::new();
}
let constraints = self.layout_constraints();
let width = clamp_with_constraints(
options.size.0.max(1),
constraints.min_width,
constraints.max_width,
options.size.0.max(1),
);
let height = clamp_with_constraints(
options.size.1.max(1),
constraints.min_height,
constraints.max_height,
options.size.1.max(1),
);
let mut child_options = options.clone();
child_options.size = (width, height);
child_options.max_width = width;
child_options.max_height = height;
self.child.render_styled(console, &child_options)
}
fn render_with_debug(
&self,
console: &Console,
options: &ConsoleOptions,
_debug: &DebugLayout,
) -> Segments {
Widget::render(self, console, options)
}
fn on_mount(&mut self) {
if !self.child_extracted {
self.child.on_mount();
}
}
fn on_unmount(&mut self) {
if !self.child_extracted {
self.child.on_unmount();
}
}
fn on_tick(&mut self, tick: u64) {
if !self.child_extracted {
self.child.on_tick(tick);
}
}
fn on_resize(&mut self, width: u16, height: u16) {
if !self.child_extracted {
self.child.on_resize(width, height);
}
}
fn on_layout(&mut self, width: u16, height: u16) {
if self.child_extracted {
return;
}
let constraints = self.layout_constraints();
let width = clamp_with_constraints(
usize::from(width.max(1)),
constraints.min_width,
constraints.max_width,
usize::from(width.max(1)),
) as u16;
let height = clamp_with_constraints(
usize::from(height.max(1)),
constraints.min_height,
constraints.max_height,
usize::from(height.max(1)),
) as u16;
self.child.on_layout(width, height);
}
fn on_event_capture(&mut self, event: &Event, ctx: &mut EventCtx) {
if !self.child_extracted {
self.child.on_event_capture(event, ctx);
}
}
fn on_event(&mut self, event: &Event, ctx: &mut EventCtx) {
if !self.child_extracted {
self.child.on_event(event, ctx);
}
}
fn on_message(&mut self, message: &MessageEvent, ctx: &mut EventCtx) {
if !self.child_extracted {
self.child.on_message(message, ctx);
}
}
fn focusable(&self) -> bool {
!self.child_extracted && self.child.focusable()
}
fn set_focus(&mut self, focused: bool) {
if !self.child_extracted {
self.child.set_focus(focused);
}
}
fn layout_height(&self) -> Option<usize> {
let constraints = self.layout_constraints();
let child_height = if self.child_extracted {
self.extracted_child_layout_height
} else {
self.child.layout_height()
};
match (constraints.min_height, constraints.max_height, child_height) {
(Some(min), Some(max), Some(child)) => Some(child.max(min).min(max)),
(Some(min), Some(max), None) if min == max => Some(min),
(Some(min), _, Some(child)) => Some(child.max(min)),
(_, Some(max), Some(child)) => Some(child.min(max)),
(_, _, other) => other,
}
}
fn content_width(&self) -> Option<usize> {
if self.child_extracted {
return self.extracted_child_content_width;
}
self.child.content_width()
}
fn layout_constraints(&self) -> LayoutConstraints {
merge_constraints(self.styles.layout, self.constraints)
}
fn styles(&self) -> Option<&WidgetStyles> {
Some(&self.styles)
}
fn styles_mut(&mut self) -> Option<&mut WidgetStyles> {
Some(&mut self.styles)
}
}
impl Renderable for Constrained {
fn render(&self, console: &Console, options: &ConsoleOptions) -> Segments {
Widget::render(self, console, options)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constrained_extraction_returns_child() {
let mut c = Constrained::new(Spacer::new(1));
let children = c.take_composed_children();
assert_eq!(children.len(), 1);
}
#[test]
fn constrained_extraction_idempotent() {
let mut c = Constrained::new(Spacer::new(1));
let _ = c.take_composed_children();
assert!(c.take_composed_children().is_empty());
}
#[test]
fn constrained_render_after_extraction() {
let mut c = Constrained::new(Spacer::new(1));
let _ = c.take_composed_children();
let console = Console::new();
let options = ConsoleOptions {
size: (20, 5),
max_width: 20,
..Default::default()
};
let segments = Widget::render(&c, &console, &options);
assert!(segments.is_empty());
}
}