use super::*;
#[derive(Debug, Clone)]
pub(crate) struct OverlayLayer {
pub(crate) node: LayoutNode,
pub(crate) modal: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum NodeKind {
Text,
Container(Direction),
Spacer,
RawDraw(usize),
}
#[derive(Debug, Clone)]
pub(crate) struct LayoutNode {
pub(crate) kind: NodeKind,
pub(crate) content: Option<String>,
pub(crate) cursor_offset: Option<usize>,
pub(crate) style: Style,
pub(crate) grow: u16,
pub(crate) align: Align,
pub(crate) align_self: Option<Align>,
pub(crate) justify: Justify,
pub(crate) wrap: bool,
pub(crate) truncate: bool,
pub(crate) gap: u32,
pub(crate) border: Option<Border>,
pub(crate) border_sides: BorderSides,
pub(crate) border_style: Style,
pub(crate) bg_color: Option<Color>,
pub(crate) padding: Padding,
pub(crate) margin: Margin,
pub(crate) constraints: Constraints,
pub(crate) title: Option<(String, Style)>,
pub(crate) children: Vec<LayoutNode>,
pub(crate) pos: (u32, u32),
pub(crate) size: (u32, u32),
pub(crate) is_scrollable: bool,
pub(crate) scroll_offset: u32,
pub(crate) content_height: u32,
pub(crate) cached_wrap_width: Option<u32>,
pub(crate) cached_wrapped: Option<Vec<String>>,
pub(crate) segments: Option<Vec<(String, Style)>>,
pub(crate) cached_wrapped_segments: Option<Vec<Vec<(String, Style)>>>,
pub(crate) focus_id: Option<usize>,
pub(crate) interaction_id: Option<usize>,
pub(crate) link_url: Option<String>,
pub(crate) group_name: Option<std::sync::Arc<str>>,
pub(crate) overlays: Vec<OverlayLayer>,
}
#[derive(Debug, Clone)]
pub(crate) struct ContainerConfig {
pub(crate) gap: u32,
pub(crate) align: Align,
pub(crate) align_self: Option<Align>,
pub(crate) justify: Justify,
pub(crate) border: Option<Border>,
pub(crate) border_sides: BorderSides,
pub(crate) border_style: Style,
pub(crate) bg_color: Option<Color>,
pub(crate) padding: Padding,
pub(crate) margin: Margin,
pub(crate) constraints: Constraints,
pub(crate) title: Option<(String, Style)>,
pub(crate) grow: u16,
}
impl LayoutNode {
pub(crate) fn text(
content: String,
style: Style,
grow: u16,
align: Align,
text_meta: (Option<usize>, bool, bool),
margin: Margin,
constraints: Constraints,
) -> Self {
let (cursor_offset, wrap, truncate) = text_meta;
let width = UnicodeWidthStr::width(content.as_str()) as u32;
Self {
kind: NodeKind::Text,
content: Some(content),
cursor_offset,
style,
grow,
align,
align_self: None,
justify: Justify::Start,
wrap,
truncate,
gap: 0,
border: None,
border_sides: BorderSides::all(),
border_style: Style::new(),
bg_color: None,
padding: Padding::default(),
margin,
constraints,
title: None,
children: Vec::new(),
pos: (0, 0),
size: (width, 1),
is_scrollable: false,
scroll_offset: 0,
content_height: 0,
cached_wrap_width: None,
cached_wrapped: None,
segments: None,
cached_wrapped_segments: None,
focus_id: None,
interaction_id: None,
link_url: None,
group_name: None,
overlays: Vec::new(),
}
}
pub(crate) fn rich_text(
segments: Vec<(String, Style)>,
wrap: bool,
align: Align,
margin: Margin,
constraints: Constraints,
) -> Self {
let width: u32 = segments
.iter()
.map(|(s, _)| UnicodeWidthStr::width(s.as_str()) as u32)
.sum();
Self {
kind: NodeKind::Text,
content: None,
cursor_offset: None,
style: Style::new(),
grow: 0,
align,
align_self: None,
justify: Justify::Start,
wrap,
truncate: false,
gap: 0,
border: None,
border_sides: BorderSides::all(),
border_style: Style::new(),
bg_color: None,
padding: Padding::default(),
margin,
constraints,
title: None,
children: Vec::new(),
pos: (0, 0),
size: (width, 1),
is_scrollable: false,
scroll_offset: 0,
content_height: 0,
cached_wrap_width: None,
cached_wrapped: None,
segments: Some(segments),
cached_wrapped_segments: None,
focus_id: None,
interaction_id: None,
link_url: None,
group_name: None,
overlays: Vec::new(),
}
}
pub(crate) fn container(direction: Direction, config: ContainerConfig) -> Self {
Self {
kind: NodeKind::Container(direction),
content: None,
cursor_offset: None,
style: Style::new(),
grow: config.grow,
align: config.align,
align_self: config.align_self,
justify: config.justify,
wrap: false,
truncate: false,
gap: config.gap,
border: config.border,
border_sides: config.border_sides,
border_style: config.border_style,
bg_color: config.bg_color,
padding: config.padding,
margin: config.margin,
constraints: config.constraints,
title: config.title,
children: Vec::new(),
pos: (0, 0),
size: (0, 0),
is_scrollable: false,
scroll_offset: 0,
content_height: 0,
cached_wrap_width: None,
cached_wrapped: None,
segments: None,
cached_wrapped_segments: None,
focus_id: None,
interaction_id: None,
link_url: None,
group_name: None,
overlays: Vec::new(),
}
}
pub(crate) fn raw_draw(
draw_id: usize,
constraints: Constraints,
grow: u16,
margin: Margin,
focus_id: Option<usize>,
interaction_id: Option<usize>,
) -> Self {
Self {
kind: NodeKind::RawDraw(draw_id),
content: None,
cursor_offset: None,
style: Style::new(),
grow,
align: Align::Start,
align_self: None,
justify: Justify::Start,
wrap: false,
truncate: false,
gap: 0,
border: None,
border_sides: BorderSides::all(),
border_style: Style::new(),
bg_color: None,
padding: Padding::default(),
margin,
constraints,
title: None,
children: Vec::new(),
pos: (0, 0),
size: (
constraints.min_width.unwrap_or(0),
constraints.min_height.unwrap_or(0),
),
is_scrollable: false,
scroll_offset: 0,
content_height: 0,
cached_wrap_width: None,
cached_wrapped: None,
segments: None,
cached_wrapped_segments: None,
focus_id,
interaction_id,
link_url: None,
group_name: None,
overlays: Vec::new(),
}
}
pub(crate) fn spacer(grow: u16) -> Self {
Self {
kind: NodeKind::Spacer,
content: None,
cursor_offset: None,
style: Style::new(),
grow,
align: Align::Start,
align_self: None,
justify: Justify::Start,
wrap: false,
truncate: false,
gap: 0,
border: None,
border_sides: BorderSides::all(),
border_style: Style::new(),
bg_color: None,
padding: Padding::default(),
margin: Margin::default(),
constraints: Constraints::default(),
title: None,
children: Vec::new(),
pos: (0, 0),
size: (0, 0),
is_scrollable: false,
scroll_offset: 0,
content_height: 0,
cached_wrap_width: None,
cached_wrapped: None,
segments: None,
cached_wrapped_segments: None,
focus_id: None,
interaction_id: None,
link_url: None,
group_name: None,
overlays: Vec::new(),
}
}
pub(crate) fn border_inset(&self) -> u32 {
if self.border.is_some() {
1
} else {
0
}
}
pub(crate) fn border_left_inset(&self) -> u32 {
if self.border.is_some() && self.border_sides.left {
1
} else {
0
}
}
pub(crate) fn border_right_inset(&self) -> u32 {
if self.border.is_some() && self.border_sides.right {
1
} else {
0
}
}
pub(crate) fn border_top_inset(&self) -> u32 {
if self.border.is_some() && self.border_sides.top {
1
} else {
0
}
}
pub(crate) fn border_bottom_inset(&self) -> u32 {
if self.border.is_some() && self.border_sides.bottom {
1
} else {
0
}
}
pub(crate) fn frame_horizontal(&self) -> u32 {
self.padding.horizontal() + self.border_left_inset() + self.border_right_inset()
}
pub(crate) fn frame_vertical(&self) -> u32 {
self.padding.vertical() + self.border_top_inset() + self.border_bottom_inset()
}
pub(crate) fn min_width(&self) -> u32 {
let width = match self.kind {
NodeKind::Text => self.size.0,
NodeKind::Spacer | NodeKind::RawDraw(_) => 0,
NodeKind::Container(Direction::Row) => {
let gaps = if self.children.is_empty() {
0
} else {
(self.children.len() as u32 - 1) * self.gap
};
let children_width: u32 = self.children.iter().map(|c| c.min_width()).sum();
children_width + gaps + self.frame_horizontal()
}
NodeKind::Container(Direction::Column) => {
self.children
.iter()
.map(|c| c.min_width())
.max()
.unwrap_or(0)
+ self.frame_horizontal()
}
};
let width = width.max(self.constraints.min_width.unwrap_or(0));
let width = match self.constraints.max_width {
Some(max_w) => width.min(max_w),
None => width,
};
width.saturating_add(self.margin.horizontal())
}
pub(crate) fn min_height(&self) -> u32 {
let height = match self.kind {
NodeKind::Text => 1,
NodeKind::Spacer | NodeKind::RawDraw(_) => 0,
NodeKind::Container(Direction::Row) => {
self.children
.iter()
.map(|c| c.min_height())
.max()
.unwrap_or(0)
+ self.frame_vertical()
}
NodeKind::Container(Direction::Column) => {
let gaps = if self.children.is_empty() {
0
} else {
(self.children.len() as u32 - 1) * self.gap
};
let children_height: u32 = self.children.iter().map(|c| c.min_height()).sum();
children_height + gaps + self.frame_vertical()
}
};
let height = height.max(self.constraints.min_height.unwrap_or(0));
height.saturating_add(self.margin.vertical())
}
pub(crate) fn ensure_wrapped_for_width(&mut self, available_width: u32) -> u32 {
if self.cached_wrap_width == Some(available_width) {
if let Some(ref segs) = self.cached_wrapped_segments {
return segs.len().max(1) as u32;
}
if let Some(ref lines) = self.cached_wrapped {
return lines.len().max(1) as u32;
}
}
if let Some(ref segs) = self.segments {
let wrapped = wrap_segments(segs, available_width);
let line_count = wrapped.len().max(1) as u32;
self.cached_wrap_width = Some(available_width);
self.cached_wrapped_segments = Some(wrapped);
self.cached_wrapped = None;
line_count
} else {
let text = self.content.as_deref().unwrap_or("");
let lines = wrap_lines(text, available_width);
let line_count = lines.len().max(1) as u32;
self.cached_wrap_width = Some(available_width);
self.cached_wrapped = Some(lines);
self.cached_wrapped_segments = None;
line_count
}
}
pub(crate) fn min_height_for_width(&mut self, available_width: u32) -> u32 {
match self.kind {
NodeKind::Text if self.wrap => {
let inner_width = available_width.saturating_sub(self.margin.horizontal());
let lines = self.ensure_wrapped_for_width(inner_width);
lines.saturating_add(self.margin.vertical())
}
_ => self.min_height(),
}
}
}
pub(crate) fn wrap_lines(text: &str, max_width: u32) -> Vec<String> {
if text.is_empty() {
return vec![String::new()];
}
if max_width == 0 {
return vec![text.to_string()];
}
fn split_long_word(
text: &str,
word_start: usize,
word_end: usize,
max_width: u32,
out: &mut Vec<((usize, usize), u32)>,
) {
out.clear();
let slice = &text[word_start..word_end];
let mut chunk_start = word_start;
let mut chunk_end = word_start;
let mut chunk_width: u32 = 0;
for (rel_i, ch) in slice.char_indices() {
let abs_i = word_start + rel_i;
let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
let ch_len = ch.len_utf8();
if chunk_end == chunk_start {
if ch_width > max_width {
out.push(((abs_i, abs_i + ch_len), ch_width));
chunk_start = abs_i + ch_len;
chunk_end = abs_i + ch_len;
chunk_width = 0;
} else {
chunk_start = abs_i;
chunk_end = abs_i + ch_len;
chunk_width = ch_width;
}
continue;
}
if chunk_width + ch_width > max_width {
out.push(((chunk_start, chunk_end), chunk_width));
if ch_width > max_width {
out.push(((abs_i, abs_i + ch_len), ch_width));
chunk_start = abs_i + ch_len;
chunk_end = abs_i + ch_len;
chunk_width = 0;
} else {
chunk_start = abs_i;
chunk_end = abs_i + ch_len;
chunk_width = ch_width;
}
} else {
chunk_end = abs_i + ch_len;
chunk_width += ch_width;
}
}
if chunk_end > chunk_start {
out.push(((chunk_start, chunk_end), chunk_width));
}
}
fn flush_line(
text: &str,
lines: &mut Vec<String>,
current_line_words: &mut Vec<(usize, usize)>,
) {
if current_line_words.is_empty() {
return;
}
let n = current_line_words.len();
let mut total_bytes = n - 1; for &(start, end) in current_line_words.iter() {
total_bytes += end - start;
}
let mut s = String::with_capacity(total_bytes);
for (i, &(start, end)) in current_line_words.iter().enumerate() {
if i > 0 {
s.push(' ');
}
s.push_str(&text[start..end]);
}
lines.push(s);
current_line_words.clear();
}
#[allow(clippy::too_many_arguments)]
fn append_fitting_word(
text: &str,
lines: &mut Vec<String>,
current_line_words: &mut Vec<(usize, usize)>,
current_width: &mut u32,
word_start: usize,
word_end: usize,
word_width: u32,
max_width: u32,
) {
if current_line_words.is_empty() {
current_line_words.push((word_start, word_end));
*current_width = word_width;
} else if *current_width + 1 + word_width <= max_width {
current_line_words.push((word_start, word_end));
*current_width += 1 + word_width;
} else {
flush_line(text, lines, current_line_words);
current_line_words.push((word_start, word_end));
*current_width = word_width;
}
}
#[allow(clippy::too_many_arguments)]
fn push_word(
text: &str,
lines: &mut Vec<String>,
current_line_words: &mut Vec<(usize, usize)>,
current_width: &mut u32,
chunk_buf: &mut Vec<((usize, usize), u32)>,
word_start: usize,
word_end: usize,
word_width: u32,
max_width: u32,
) {
if word_start == word_end {
return;
}
if word_width > max_width {
split_long_word(text, word_start, word_end, max_width, chunk_buf);
for &((cs, ce), cw) in chunk_buf.iter() {
append_fitting_word(
text,
lines,
current_line_words,
current_width,
cs,
ce,
cw,
max_width,
);
}
return;
}
append_fitting_word(
text,
lines,
current_line_words,
current_width,
word_start,
word_end,
word_width,
max_width,
);
}
let mut lines: Vec<String> = Vec::new();
let mut current_line_words: Vec<(usize, usize)> = Vec::new();
let mut current_width: u32 = 0;
let mut chunk_buf: Vec<((usize, usize), u32)> = Vec::new();
let mut word_start: usize = 0;
let mut word_width: u32 = 0;
for (i, ch) in text.char_indices() {
if ch == ' ' {
push_word(
text,
&mut lines,
&mut current_line_words,
&mut current_width,
&mut chunk_buf,
word_start,
i,
word_width,
max_width,
);
word_start = i + 1; word_width = 0;
continue;
}
word_width += UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
}
push_word(
text,
&mut lines,
&mut current_line_words,
&mut current_width,
&mut chunk_buf,
word_start,
text.len(),
word_width,
max_width,
);
flush_line(text, &mut lines, &mut current_line_words);
if lines.is_empty() {
vec![String::new()]
} else {
lines
}
}
pub(crate) fn wrap_segments(
segments: &[(String, Style)],
max_width: u32,
) -> Vec<Vec<(String, Style)>> {
if max_width == 0 || segments.is_empty() {
return vec![vec![]];
}
if !segments.iter().any(|(seg_text, _)| !seg_text.is_empty()) {
return vec![vec![]];
}
fn advance_past_empty(segments: &[(String, Style)], cur_seg: &mut usize, cur_off: &mut usize) {
while *cur_seg < segments.len() && *cur_off >= segments[*cur_seg].0.len() {
*cur_seg += 1;
*cur_off = 0;
}
}
let mut lines: Vec<Vec<(String, Style)>> = Vec::new();
let mut cur_seg: usize = 0;
let mut cur_off: usize = 0;
advance_past_empty(segments, &mut cur_seg, &mut cur_off);
while cur_seg < segments.len() {
if !lines.is_empty() {
loop {
advance_past_empty(segments, &mut cur_seg, &mut cur_off);
if cur_seg >= segments.len() {
break;
}
let s = segments[cur_seg].0.as_str();
let ch = s[cur_off..]
.chars()
.next()
.expect("advance_past_empty guarantees cur_off < s.len() with a valid char");
if ch == ' ' {
cur_off += 1; continue;
}
break;
}
if cur_seg >= segments.len() {
break;
}
}
let mut line_segs: Vec<(String, Style)> = Vec::new();
let mut line_width: u32 = 0;
let mut last_space_break: Option<(usize, usize, u32, usize, usize)> = None;
loop {
advance_past_empty(segments, &mut cur_seg, &mut cur_off);
if cur_seg >= segments.len() {
break;
}
let s = segments[cur_seg].0.as_str();
let style = segments[cur_seg].1;
let ch = s[cur_off..]
.chars()
.next()
.expect("advance_past_empty guarantees cur_off < s.len() with a valid char");
let ch_len = ch.len_utf8();
let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
if line_width + ch_width > max_width && line_width > 0 {
if let Some((segs_len, last_byte_len, _w, sp_seg, sp_off)) = last_space_break {
line_segs.truncate(segs_len);
if let Some(last) = line_segs.last_mut() {
last.0.truncate(last_byte_len);
}
cur_seg = sp_seg;
cur_off = sp_off + 1; }
break;
}
if ch == ' ' {
let segs_len = line_segs.len();
let last_byte_len = line_segs.last().map(|(text, _)| text.len()).unwrap_or(0);
last_space_break = Some((segs_len, last_byte_len, line_width, cur_seg, cur_off));
}
if let Some(last) = line_segs.last_mut() {
if last.1 == style {
last.0.push(ch);
} else {
let mut nw = String::new();
nw.push(ch);
line_segs.push((nw, style));
}
} else {
let mut nw = String::new();
nw.push(ch);
line_segs.push((nw, style));
}
line_width += ch_width;
cur_off += ch_len;
}
let cascade = if let Some(last) = line_segs.last_mut() {
let trimmed_len = last.0.trim_end().len();
if trimmed_len == 0 {
true
} else {
last.0.truncate(trimmed_len);
false
}
} else {
false
};
if cascade {
line_segs.pop();
if let Some(last) = line_segs.last_mut() {
let trimmed_len = last.0.trim_end().len();
if trimmed_len == 0 {
line_segs.pop();
} else {
last.0.truncate(trimmed_len);
}
}
}
lines.push(line_segs);
}
if lines.is_empty() {
vec![vec![]]
} else {
lines
}
}
pub(crate) const MAX_LAYOUT_DEPTH: usize = 512;
pub(crate) fn build_tree(commands: Vec<Command>) -> LayoutNode {
let mut root = LayoutNode::container(Direction::Column, default_container_config());
let mut overlays: Vec<OverlayLayer> = Vec::new();
let mut commands = commands.into_iter();
build_children(&mut root, &mut commands, &mut overlays, false, 0);
root.overlays = overlays;
root
}
pub(crate) fn default_container_config() -> ContainerConfig {
ContainerConfig {
gap: 0,
align: Align::Start,
align_self: None,
justify: Justify::Start,
border: None,
border_sides: BorderSides::all(),
border_style: Style::new(),
bg_color: None,
padding: Padding::default(),
margin: Margin::default(),
constraints: Constraints::default(),
title: None,
grow: 0,
}
}
fn build_children(
parent: &mut LayoutNode,
commands: &mut std::vec::IntoIter<Command>,
overlays: &mut Vec<OverlayLayer>,
stop_on_end_overlay: bool,
depth: usize,
) {
if depth > MAX_LAYOUT_DEPTH {
panic!(
"layout tree depth exceeds {MAX_LAYOUT_DEPTH}: \
check for recursive container nesting"
);
}
let mut pending_focus_id: Option<usize> = None;
let mut pending_interaction_id: Option<usize> = None;
while let Some(command) = commands.next() {
match command {
Command::FocusMarker(id) => pending_focus_id = Some(id),
Command::InteractionMarker(id) => pending_interaction_id = Some(id),
Command::Text {
content,
cursor_offset,
style,
grow,
align,
wrap,
truncate,
margin,
constraints,
} => {
let mut node = LayoutNode::text(
content,
style,
grow,
align,
(cursor_offset, wrap, truncate),
margin,
constraints,
);
node.focus_id = pending_focus_id.take();
node.interaction_id = pending_interaction_id.take();
parent.children.push(node);
}
Command::RichText {
segments,
wrap,
align,
margin,
constraints,
} => {
let mut node = LayoutNode::rich_text(segments, wrap, align, margin, constraints);
node.focus_id = pending_focus_id.take();
node.interaction_id = pending_interaction_id.take();
parent.children.push(node);
}
Command::Link {
text,
url,
style,
margin,
constraints,
} => {
let mut node = LayoutNode::text(
text,
style,
0,
Align::Start,
(None, false, false),
margin,
constraints,
);
node.link_url = Some(url);
node.focus_id = pending_focus_id.take();
node.interaction_id = pending_interaction_id.take();
parent.children.push(node);
}
Command::BeginContainer(args) => {
let BeginContainerArgs {
direction,
gap,
align,
align_self,
justify,
border,
border_sides,
border_style,
bg_color,
padding,
margin,
constraints,
title,
grow,
group_name,
} = *args;
let mut node = LayoutNode::container(
direction,
ContainerConfig {
gap,
align,
align_self,
justify,
border,
border_sides,
border_style,
bg_color,
padding,
margin,
constraints,
title,
grow,
},
);
node.focus_id = pending_focus_id.take();
node.interaction_id = pending_interaction_id.take();
node.group_name = group_name;
build_children(&mut node, commands, overlays, false, depth + 1);
parent.children.push(node);
}
Command::BeginScrollable(args) => {
let BeginScrollableArgs {
grow,
border,
border_sides,
border_style,
bg_color,
align,
align_self,
justify,
gap,
padding,
margin,
constraints,
title,
scroll_offset,
group_name,
} = *args;
let mut node = LayoutNode::container(
Direction::Column,
ContainerConfig {
gap,
align,
align_self,
justify,
border,
border_sides,
border_style,
bg_color,
padding,
margin,
constraints,
title,
grow,
},
);
node.is_scrollable = true;
node.scroll_offset = scroll_offset;
node.focus_id = pending_focus_id.take();
node.interaction_id = pending_interaction_id.take();
node.group_name = group_name;
build_children(&mut node, commands, overlays, false, depth + 1);
parent.children.push(node);
}
Command::BeginOverlay { modal } => {
let mut overlay_node =
LayoutNode::container(Direction::Column, default_container_config());
overlay_node.interaction_id = pending_interaction_id.take();
build_children(&mut overlay_node, commands, overlays, true, depth + 1);
overlays.push(OverlayLayer {
node: overlay_node,
modal,
});
}
Command::Spacer { grow } => parent.children.push(LayoutNode::spacer(grow)),
Command::RawDraw {
draw_id,
constraints,
grow,
margin,
} => {
let node = LayoutNode::raw_draw(
draw_id,
constraints,
grow,
margin,
pending_focus_id.take(),
pending_interaction_id.take(),
);
parent.children.push(node);
}
Command::EndContainer => return,
Command::EndOverlay => {
if stop_on_end_overlay {
return;
}
}
}
}
}