use rich_rs::{Console, ConsoleOptions, MetaValue, Segments, StyleMeta};
use std::any::Any;
use crate::action::{ActionDecl, ParsedAction};
use crate::compose::ComposeResult;
use crate::debug::DebugLayout;
use crate::event::{Action, BindingHint, Event, EventCtx};
use crate::message::MessageEvent;
use crate::node_id::{self, NodeId};
use crate::reactive::ReactiveWidget;
use crate::style::{Color, HorizontalAlign, Position, Style, VerticalAlign};
use super::helpers;
const META_WIDGET_ID: &str = "textual:widget_id";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StyleChangeKind {
None,
Visual,
Layout,
}
pub fn classify_style_change(old: &Style, new: &Style) -> StyleChangeKind {
if old == new {
return StyleChangeKind::None;
}
if old.display != new.display
|| old.visibility != new.visibility
|| old.overflow != new.overflow
|| old.layout != new.layout
|| old.dock != new.dock
|| old.width != new.width
|| old.height != new.height
|| old.min_width != new.min_width
|| old.max_width != new.max_width
|| old.min_height != new.min_height
|| old.max_height != new.max_height
|| old.margin != new.margin
|| old.padding != new.padding
|| old.align != new.align
|| old.content_align != new.content_align
|| old.offset != new.offset
|| old.constrain != new.constrain
|| old.grid_size_columns != new.grid_size_columns
|| old.grid_size_rows != new.grid_size_rows
|| old.grid_columns != new.grid_columns
|| old.grid_rows != new.grid_rows
|| old.grid_gutter_horizontal != new.grid_gutter_horizontal
|| old.grid_gutter_vertical != new.grid_gutter_vertical
|| old.border != new.border
|| old.border_top != new.border_top
|| old.border_right != new.border_right
|| old.border_bottom != new.border_bottom
|| old.border_left != new.border_left
{
return StyleChangeKind::Layout;
}
StyleChangeKind::Visual
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BindingDecl {
pub key: String,
pub action: String,
pub description: String,
pub tooltip: Option<String>,
pub namespace: Option<String>,
pub show: bool,
pub priority: bool,
}
impl BindingDecl {
pub fn new(key: &str, action: &str, description: &str) -> Self {
Self {
key: key.to_string(),
action: action.to_string(),
description: description.to_string(),
tooltip: None,
namespace: None,
show: true,
priority: false,
}
}
pub fn hidden(mut self) -> Self {
self.show = false;
self
}
pub fn priority(mut self) -> Self {
self.priority = true;
self
}
pub fn with_tooltip(mut self, tooltip: impl Into<String>) -> Self {
self.tooltip = Some(tooltip.into());
self
}
pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
self.namespace = Some(namespace.into());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct WidgetSelectionAnchor {
pub row: usize,
pub col: usize,
pub index: usize,
}
pub trait Widget: Send + Sync + Any {
fn render(&self, console: &Console, options: &ConsoleOptions) -> Segments;
fn render_line(&self, y: usize, console: &Console, options: &ConsoleOptions) -> Segments {
let width = options.size.0.max(1);
let lines = rich_rs::Segment::split_and_crop_lines(
self.render(console, options),
width,
None,
true,
false,
);
lines.get(y).cloned().unwrap_or_default().into()
}
fn render_lines(
&self,
start_y: usize,
line_count: usize,
console: &Console,
options: &ConsoleOptions,
) -> Vec<Segments> {
(0..line_count)
.map(|offset| self.render_line(start_y + offset, console, options))
.collect()
}
fn compose(&self) -> ComposeResult {
Vec::new()
}
fn take_composed_children(&mut self) -> Vec<Box<dyn Widget>> {
Vec::new()
}
fn node_id(&self) -> NodeId {
crate::runtime::dispatch_ctx::dispatch_recipient().unwrap_or_default()
}
fn render_styled_dyn_obj(
&self,
console: &Console,
options: &ConsoleOptions,
debug: Option<&DebugLayout>,
_node_id: NodeId,
) -> Segments {
let _dispatch_guard = crate::runtime::dispatch_ctx::set_dispatch_recipient(_node_id);
let debug_widget_label = {
let mut label = self.style_type().to_string();
if let Some(id) = self.style_id() {
label.push('#');
label.push_str(id);
}
for class in self.style_classes() {
label.push('.');
label.push_str(class);
}
if self.is_disabled() {
label.push_str(":disabled");
}
if self.has_focus() {
label.push_str(":focus");
}
if self.is_hovered() {
label.push_str(":hover");
}
if self.is_active() {
label.push_str(":active");
}
label
};
let meta = crate::css::selector_meta_generic(self);
let resolved = crate::css::resolve_style(self, &meta);
render_widget_with_meta(
self,
console,
options,
debug,
_node_id,
&meta,
&resolved,
&debug_widget_label,
)
}
fn render_with_debug(
&self,
console: &Console,
options: &ConsoleOptions,
_debug: &DebugLayout,
) -> Segments {
self.render(console, options)
}
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 set_virtual_content_size(&mut self, _width: usize, _height: usize) {}
fn on_event_capture(&mut self, _event: &Event, _ctx: &mut EventCtx) {}
fn on_event(&mut self, _event: &Event, _ctx: &mut EventCtx) {}
fn on_message(&mut self, _message: &MessageEvent, _ctx: &mut EventCtx) {}
fn on_app_key(
&mut self,
_app: &mut crate::App,
_key: &crate::keys::KeyEventData,
_ctx: &mut EventCtx,
) {
}
fn on_app_action(&mut self, _app: &mut crate::App, _action: Action, _ctx: &mut EventCtx) {}
fn on_app_message(
&mut self,
_app: &mut crate::App,
_message: &MessageEvent,
_ctx: &mut EventCtx,
) {
}
fn on_app_tick(&mut self, _app: &mut crate::App, _tick: u64, _ctx: &mut EventCtx) {}
fn on_app_mount(&mut self, _app: &mut crate::App, _ctx: &mut EventCtx) {}
fn child_display_for_tree(&self, _child_index: usize) -> Option<bool> {
None
}
fn tree_child_content_inset(&self) -> (u16, u16, u16, u16) {
(0, 0, 0, 0)
}
fn reactive_widget(&mut self) -> Option<&mut dyn ReactiveWidget> {
None
}
fn binding_hints(&self) -> Vec<BindingHint> {
Vec::new()
}
fn bindings(&self) -> Vec<BindingDecl> {
Vec::new()
}
fn action_namespace(&self) -> &str {
""
}
fn action_registry(&self) -> &[ActionDecl] {
&[]
}
fn execute_action(&mut self, _action: &ParsedAction, _ctx: &mut EventCtx) -> bool {
false
}
fn help_markup(&self) -> Option<&str> {
None
}
fn tooltip(&self) -> Option<String> {
None
}
fn tooltip_anchor(&self) -> Option<(u16, u16)> {
None
}
fn on_mouse_move(&mut self, _x: u16, _y: u16) -> bool {
false
}
fn allow_select(&self) -> bool {
false
}
fn selection_at(&self, _x: u16, _y: u16) -> Option<WidgetSelectionAnchor> {
None
}
fn selection_word_range_at(
&self,
_x: u16,
_y: u16,
) -> Option<(WidgetSelectionAnchor, WidgetSelectionAnchor)> {
None
}
fn selection_all_range(&self) -> Option<(WidgetSelectionAnchor, WidgetSelectionAnchor)> {
None
}
fn update_selection(
&mut self,
_from: WidgetSelectionAnchor,
_to: WidgetSelectionAnchor,
) -> bool {
false
}
fn clear_selection(&mut self) -> bool {
false
}
fn get_selection(&self) -> Option<String> {
None
}
fn selection_updated(&mut self, _ctx: &mut EventCtx) {}
fn scroll_offset(&self) -> (usize, usize) {
(0, 0)
}
fn scroll_offset_f32(&self) -> (f32, f32) {
let (x, y) = self.scroll_offset();
(x as f32, y as f32)
}
fn scroll_viewport_size(&self) -> Option<(usize, usize)> {
None
}
fn scroll_virtual_content_size(&self) -> Option<(usize, usize)> {
None
}
fn clips_descendants_to_content(&self) -> bool {
false
}
fn on_mouse_scroll(&mut self, _delta_x: i32, _delta_y: i32, _ctx: &mut EventCtx) {}
fn focusable(&self) -> bool {
false
}
fn can_focus(&self) -> bool {
self.focusable()
}
fn can_focus_children(&self) -> bool {
true
}
fn set_focus(&mut self, _focused: bool) {}
fn is_disabled(&self) -> bool {
false
}
fn set_disabled_state(&mut self, _disabled: bool) {}
fn is_loading(&self) -> bool {
false
}
fn set_loading_state(&mut self, _loading: bool) {}
fn has_focus(&self) -> bool {
false
}
fn is_hovered(&self) -> bool {
false
}
fn set_hovered(&mut self, _hovered: bool) {}
fn mouse_interactive(&self) -> bool {
self.focusable()
}
fn is_active(&self) -> bool {
false
}
fn preserve_underlay(&self) -> bool {
false
}
fn content_width(&self) -> Option<usize> {
None
}
fn layout_height(&self) -> Option<usize> {
helpers::fixed_height_from_constraints(self.layout_constraints())
}
fn layout_constraints(&self) -> LayoutConstraints {
self.styles()
.map(|styles| styles.layout)
.unwrap_or_default()
}
fn style(&self) -> Option<Style> {
self.styles().map(|styles| styles.style.clone())
}
fn styles(&self) -> Option<&WidgetStyles> {
None
}
fn styles_mut(&mut self) -> Option<&mut WidgetStyles> {
None
}
fn style_type(&self) -> &'static str {
std::any::type_name::<Self>()
.rsplit("::")
.next()
.unwrap_or("Widget")
}
fn style_type_aliases(&self) -> &[&'static str] {
&[]
}
fn style_id(&self) -> Option<&str> {
self.styles().and_then(|styles| styles.style_id.as_deref())
}
fn style_classes(&self) -> &[String] {
helpers::empty_classes()
}
fn set_style_id(&mut self, id: Option<String>) {
if let Some(styles) = self.styles_mut() {
styles.style_id = id;
}
}
fn border_title(&self) -> Option<&str> {
None
}
fn border_subtitle(&self) -> Option<&str> {
None
}
fn render_styled(&self, console: &Console, options: &ConsoleOptions) -> Segments {
self.render_styled_dyn_obj(console, options, None, NodeId::default())
}
fn render_styled_with_debug(
&self,
console: &Console,
options: &ConsoleOptions,
debug: &DebugLayout,
) -> Segments {
self.render_styled_dyn_obj(console, options, Some(debug), NodeId::default())
}
fn set_width(&mut self, value: usize) {
if let Some(styles) = self.styles_mut() {
styles.set_width(value);
}
}
fn set_height(&mut self, value: usize) {
if let Some(styles) = self.styles_mut() {
styles.set_height(value);
}
}
fn set_min_width(&mut self, value: usize) {
if let Some(styles) = self.styles_mut() {
styles.set_min_width(value);
}
}
fn set_max_width(&mut self, value: usize) {
if let Some(styles) = self.styles_mut() {
styles.set_max_width(value);
}
}
fn set_min_height(&mut self, value: usize) {
if let Some(styles) = self.styles_mut() {
styles.set_min_height(value);
}
}
fn set_max_height(&mut self, value: usize) {
if let Some(styles) = self.styles_mut() {
styles.set_max_height(value);
}
}
}
fn tag_widget_meta(node_id: NodeId, segments: Segments) -> Segments {
let ffi_value = node_id::node_id_to_ffi(node_id) as i64;
tag_widget_meta_raw(ffi_value, segments)
}
fn tag_widget_meta_raw(ffi_value: i64, segments: Segments) -> Segments {
let mut out = Segments::new();
for mut seg in segments {
if seg.control.is_some() {
out.push(seg);
continue;
}
let has_widget_id = seg
.meta
.as_ref()
.and_then(|m| m.meta.as_ref())
.map(|map| map.contains_key(META_WIDGET_ID))
.unwrap_or(false);
if has_widget_id {
out.push(seg);
continue;
}
let mut map = seg
.meta
.as_ref()
.and_then(|m| m.meta.as_ref())
.map(|m| (**m).clone())
.unwrap_or_default();
map.insert(META_WIDGET_ID.to_string(), MetaValue::Int(ffi_value));
let mut meta = seg.meta.unwrap_or_else(StyleMeta::new);
meta.meta = Some(std::sync::Arc::new(map));
seg.meta = Some(meta);
out.push(seg);
}
out
}
#[derive(Debug, Clone, Copy, Default)]
pub struct LayoutConstraints {
pub min_width: Option<usize>,
pub max_width: Option<usize>,
pub min_height: Option<usize>,
pub max_height: Option<usize>,
}
pub(crate) fn render_widget_with_meta<W: Widget + ?Sized>(
widget: &W,
console: &Console,
options: &ConsoleOptions,
debug: Option<&DebugLayout>,
node_id: NodeId,
meta: &crate::css::SelectorMeta,
resolved: &Style,
debug_widget_label: &str,
) -> Segments {
let parent_style = crate::css::current_parent_style();
let line_pad = resolved.line_pad.unwrap_or(0) as usize;
let full_width = options.size.0.max(1);
let full_height = options.size.1.max(1);
let (border_top, border_bottom, border_left, border_right) =
helpers::border_spacing_from_style(resolved);
let padding = resolved.effective_padding();
let content_width = full_width
.saturating_sub(border_left + border_right)
.saturating_sub(line_pad.saturating_mul(2))
.saturating_sub(padding.left as usize + padding.right as usize)
.max(1);
let content_height = full_height
.saturating_sub(border_top + border_bottom)
.saturating_sub(padding.top as usize + padding.bottom as usize)
.max(1);
let mut content_options = options.clone();
content_options.size = (content_width, content_height);
content_options.max_width = content_width;
content_options.max_height = content_height;
let segments = crate::css::with_style_stack(meta.clone(), resolved.clone(), || match debug {
Some(debug) => widget.render_with_debug(console, &content_options, debug),
None => widget.render(console, &content_options),
});
let segments = tag_widget_meta(node_id, segments);
let segments_empty = segments.is_empty();
let inner_width = content_width
.saturating_add(line_pad.saturating_mul(2))
.saturating_add(padding.left as usize + padding.right as usize)
.max(1);
let mut lines = if line_pad > 0 {
let padded = helpers::apply_line_pad(
segments,
content_width,
content_width + line_pad * 2,
line_pad,
);
rich_rs::Segment::split_and_crop_lines(
padded,
content_width + line_pad * 2,
None,
false,
false,
)
} else {
rich_rs::Segment::split_and_crop_lines(segments, content_width, None, false, false)
};
if let Some(content_align) = resolved.content_align {
lines = apply_content_alignment(
lines,
content_width + line_pad * 2,
content_height,
content_align.horizontal,
content_align.vertical,
);
}
let has_surface_paint = 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();
let preserve_underlay = widget.preserve_underlay()
|| (resolved.position == Some(Position::Absolute) && !has_surface_paint)
|| (!has_surface_paint && segments_empty);
if preserve_underlay && segments_empty {
return Segments::new();
}
let mut final_lines: Vec<Vec<rich_rs::Segment>> = Vec::new();
if preserve_underlay {
final_lines.extend(lines);
} else {
let mut fill = rich_rs::Style::new();
let fallback_bg = crate::style::parse_color_like("$background")
.unwrap_or(crate::style::Color::rgb(0, 0, 0));
let parent_bg = crate::css::current_composited_background()
.or_else(|| parent_style.clone().and_then(|s| s.bg))
.unwrap_or(fallback_bg);
let inner_bg = resolved
.bg
.map(|c| c.flatten_over(parent_bg))
.unwrap_or(parent_bg);
fill = fill.with_bgcolor(inner_bg.to_simple_opaque());
if let Some(fg) = resolved.fg {
fill = fill.with_color(fg.flatten_over(inner_bg).to_simple_opaque());
}
lines = rich_rs::Segment::set_shape(
&lines,
content_width + line_pad * 2,
Some(content_height),
Some(fill),
false,
);
let pad_left = padding.left as usize;
let pad_right = padding.right as usize;
let mut padded_lines: Vec<Vec<rich_rs::Segment>> = Vec::with_capacity(lines.len());
for line in lines {
let mut out = Vec::new();
if pad_left > 0 {
out.push(rich_rs::Segment::styled(" ".repeat(pad_left), fill));
}
out.extend(line);
if pad_right > 0 {
out.push(rich_rs::Segment::styled(" ".repeat(pad_right), fill));
}
padded_lines.push(out);
}
let pad_top = padding.top as usize;
let pad_bottom = padding.bottom as usize;
let padded_width = content_width + line_pad * 2 + pad_left + pad_right;
if pad_top > 0 {
let blank = vec![rich_rs::Segment::styled(" ".repeat(padded_width), fill)];
for _ in 0..pad_top {
final_lines.push(blank.clone());
}
}
final_lines.extend(padded_lines);
if pad_bottom > 0 {
let blank = vec![rich_rs::Segment::styled(" ".repeat(padded_width), fill)];
for _ in 0..pad_bottom {
final_lines.push(blank.clone());
}
}
}
let mut segments = Segments::new();
let line_count = final_lines.len();
for (idx, line) in final_lines.into_iter().enumerate() {
segments.extend(line);
if idx + 1 < line_count {
segments.push(rich_rs::Segment::line());
}
}
let styled = crate::css::apply_style_to_segments(
node_id,
segments,
resolved.clone(),
parent_style.clone(),
);
let segments = helpers::apply_border_edges(
styled,
inner_width,
resolved.clone(),
parent_style.clone(),
full_width,
full_height,
debug_widget_label,
widget.border_title(),
widget.border_subtitle(),
);
let segments = if let Some(opacity) = resolved.opacity {
crate::css::apply_widget_opacity_to_segments(segments, opacity, parent_style)
} else {
segments
};
tag_widget_meta(node_id, segments)
}
fn apply_content_alignment(
lines: Vec<Vec<rich_rs::Segment>>,
content_width: usize,
content_height: usize,
horizontal: HorizontalAlign,
vertical: VerticalAlign,
) -> Vec<Vec<rich_rs::Segment>> {
let mut aligned: Vec<Vec<rich_rs::Segment>> = Vec::with_capacity(lines.len());
for mut line in lines {
let has_synthetic_padding = line.iter().any(|segment| {
segment
.meta
.as_ref()
.and_then(|meta| meta.meta.as_ref())
.and_then(|meta| meta.get("textual:no_text_style"))
.is_some_and(|value| matches!(value, MetaValue::Bool(true)))
});
let line_width = if has_synthetic_padding {
rich_rs::Segment::get_line_length(&line).min(content_width)
} else {
let line_text = line
.iter()
.map(|segment| segment.text.as_ref())
.collect::<String>();
rich_rs::cell_len(line_text.trim_end()).min(content_width)
};
let left_pad = match horizontal {
HorizontalAlign::Left => 0,
HorizontalAlign::Center => content_width.saturating_sub(line_width) / 2,
HorizontalAlign::Right => content_width.saturating_sub(line_width),
};
if left_pad > 0 {
line.insert(0, rich_rs::Segment::new(" ".repeat(left_pad)));
}
aligned.push(helpers::adjust_line_length_no_bg(&line, content_width));
}
if aligned.len() >= content_height {
aligned.truncate(content_height);
return aligned;
}
let extra_rows = content_height.saturating_sub(aligned.len());
let top_pad = match vertical {
VerticalAlign::Top => 0,
VerticalAlign::Middle => extra_rows / 2,
VerticalAlign::Bottom => extra_rows,
};
if top_pad == 0 {
return aligned;
}
let mut out = Vec::with_capacity(top_pad + aligned.len());
for _ in 0..top_pad {
out.push(Vec::new());
}
out.extend(aligned);
out
}
impl LayoutConstraints {
pub fn new() -> Self {
Self::default()
}
pub fn min_width(mut self, value: usize) -> Self {
self.min_width = Some(value.max(1));
self
}
pub fn max_width(mut self, value: usize) -> Self {
self.max_width = Some(value.max(1));
self
}
pub fn min_height(mut self, value: usize) -> Self {
self.min_height = Some(value.max(1));
self
}
pub fn max_height(mut self, value: usize) -> Self {
self.max_height = Some(value.max(1));
self
}
}
#[derive(Debug, Clone, Default)]
pub struct WidgetStyles {
pub style: Style,
pub layout: LayoutConstraints,
pub style_id: Option<String>,
}
impl WidgetStyles {
pub fn new() -> Self {
Self::default()
}
pub fn fg(mut self, color: Color) -> Self {
self.style = self.style.fg(color);
self
}
pub fn bg(mut self, color: Color) -> Self {
self.style = self.style.bg(color);
self
}
pub fn bold(mut self, value: bool) -> Self {
self.style = self.style.bold(value);
self
}
pub fn dim(mut self, value: bool) -> Self {
self.style = self.style.dim(value);
self
}
pub fn italic(mut self, value: bool) -> Self {
self.style = self.style.italic(value);
self
}
pub fn underline(mut self, value: bool) -> Self {
self.style = self.style.underline(value);
self
}
pub fn border(mut self, value: bool) -> Self {
self.style = self.style.border(value);
self
}
pub fn set_fg(&mut self, color: Color) {
self.style = std::mem::take(&mut self.style).fg(color);
}
pub fn set_bg(&mut self, color: Color) {
self.style = std::mem::take(&mut self.style).bg(color);
}
pub fn set_bold(&mut self, value: bool) {
self.style = std::mem::take(&mut self.style).bold(value);
}
pub fn set_dim(&mut self, value: bool) {
self.style = std::mem::take(&mut self.style).dim(value);
}
pub fn set_italic(&mut self, value: bool) {
self.style = std::mem::take(&mut self.style).italic(value);
}
pub fn set_underline(&mut self, value: bool) {
self.style = std::mem::take(&mut self.style).underline(value);
}
pub fn set_border(&mut self, value: bool) {
self.style = std::mem::take(&mut self.style).border(value);
}
pub fn set_style_id(&mut self, id: impl Into<String>) {
self.style_id = Some(id.into());
}
pub fn clear_style_id(&mut self) {
self.style_id = None;
}
pub fn width(mut self, value: usize) -> Self {
let value = value.max(1);
self.layout.min_width = Some(value);
self.layout.max_width = Some(value);
self
}
pub fn height(mut self, value: usize) -> Self {
let value = value.max(1);
self.layout.min_height = Some(value);
self.layout.max_height = Some(value);
self
}
pub fn min_width(mut self, value: usize) -> Self {
self.layout.min_width = Some(value.max(1));
self
}
pub fn max_width(mut self, value: usize) -> Self {
self.layout.max_width = Some(value.max(1));
self
}
pub fn min_height(mut self, value: usize) -> Self {
self.layout.min_height = Some(value.max(1));
self
}
pub fn max_height(mut self, value: usize) -> Self {
self.layout.max_height = Some(value.max(1));
self
}
pub fn set_width(&mut self, value: usize) {
let value = value.max(1);
self.layout.min_width = Some(value);
self.layout.max_width = Some(value);
}
pub fn set_height(&mut self, value: usize) {
let value = value.max(1);
self.layout.min_height = Some(value);
self.layout.max_height = Some(value);
}
pub fn set_min_width(&mut self, value: usize) {
self.layout.min_width = Some(value.max(1));
}
pub fn set_max_width(&mut self, value: usize) {
self.layout.max_width = Some(value.max(1));
}
pub fn set_min_height(&mut self, value: usize) {
self.layout.min_height = Some(value.max(1));
}
pub fn set_max_height(&mut self, value: usize) {
self.layout.max_height = Some(value.max(1));
}
pub fn invalidation_kind(&self, other: &WidgetStyles) -> StyleChangeKind {
if self.style_id != other.style_id {
return StyleChangeKind::Layout;
}
classify_style_change(&self.style, &other.style)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::node_id::NodeId;
use crate::runtime::dispatch_ctx::{dispatch_recipient, set_dispatch_recipient};
use crate::style::{Display, Layout, Overflow, Visibility};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
#[test]
fn classify_identical_styles_returns_none() {
let s = Style::default();
assert_eq!(classify_style_change(&s, &s), StyleChangeKind::None);
}
#[test]
fn classify_visual_only_change_returns_visual() {
let old = Style::default();
let mut new = old.clone();
new.fg = Some(crate::style::Color::rgb(255, 0, 0));
assert_eq!(classify_style_change(&old, &new), StyleChangeKind::Visual);
}
#[test]
fn classify_bg_change_returns_visual() {
let old = Style::default();
let mut new = old.clone();
new.bg = Some(crate::style::Color::rgb(0, 0, 255));
assert_eq!(classify_style_change(&old, &new), StyleChangeKind::Visual);
}
#[test]
fn classify_bold_change_returns_visual() {
let old = Style::default();
let mut new = old.clone();
new.bold = Some(true);
assert_eq!(classify_style_change(&old, &new), StyleChangeKind::Visual);
}
#[test]
fn classify_border_change_returns_layout() {
let old = Style::default();
let mut new = old.clone();
new.border = Some(true);
assert_eq!(classify_style_change(&old, &new), StyleChangeKind::Layout);
}
#[test]
fn classify_opacity_change_returns_visual() {
let old = Style::default();
let mut new = old.clone();
new.opacity = Some(128);
assert_eq!(classify_style_change(&old, &new), StyleChangeKind::Visual);
}
#[test]
fn classify_display_change_returns_layout() {
let old = Style::default();
let mut new = old.clone();
new.display = Some(Display::None);
assert_eq!(classify_style_change(&old, &new), StyleChangeKind::Layout);
}
#[test]
fn classify_visibility_change_returns_layout() {
let old = Style::default();
let mut new = old.clone();
new.visibility = Some(Visibility::Hidden);
assert_eq!(classify_style_change(&old, &new), StyleChangeKind::Layout);
}
#[test]
fn classify_overflow_change_returns_layout() {
let old = Style::default();
let mut new = old.clone();
new.overflow = Some(Overflow::Hidden);
assert_eq!(classify_style_change(&old, &new), StyleChangeKind::Layout);
}
#[test]
fn classify_layout_change_returns_layout() {
let old = Style::default();
let mut new = old.clone();
new.layout = Some(Layout::Horizontal);
assert_eq!(classify_style_change(&old, &new), StyleChangeKind::Layout);
}
#[test]
fn classify_width_change_returns_layout() {
let old = Style::default();
let mut new = old.clone();
new.width = Some(crate::style::Scalar::Cells(42));
assert_eq!(classify_style_change(&old, &new), StyleChangeKind::Layout);
}
#[test]
fn classify_margin_change_returns_layout() {
let old = Style::default();
let mut new = old.clone();
new.margin = Some(crate::style::Spacing {
top: 1,
right: 1,
bottom: 1,
left: 1,
});
assert_eq!(classify_style_change(&old, &new), StyleChangeKind::Layout);
}
#[test]
fn classify_padding_change_returns_layout() {
let old = Style::default();
let mut new = old.clone();
new.padding = Some(crate::style::Spacing {
top: 2,
right: 0,
bottom: 2,
left: 0,
});
assert_eq!(classify_style_change(&old, &new), StyleChangeKind::Layout);
}
#[test]
fn classify_mixed_changes_returns_layout() {
let old = Style::default();
let mut new = old.clone();
new.fg = Some(crate::style::Color::rgb(255, 0, 0)); new.display = Some(Display::None); assert_eq!(classify_style_change(&old, &new), StyleChangeKind::Layout);
}
#[test]
fn classify_layer_change_returns_visual() {
let old = Style::default();
let mut new = old.clone();
new.layer = Some("overlay".into());
assert_eq!(classify_style_change(&old, &new), StyleChangeKind::Visual);
}
#[test]
fn widget_styles_invalidation_kind_delegates() {
let a = WidgetStyles::default();
let mut b = WidgetStyles::default();
b.style.bg = Some(crate::style::Color::rgb(0, 255, 0));
assert_eq!(a.invalidation_kind(&b), StyleChangeKind::Visual);
}
fn make_node_id() -> NodeId {
let mut sm: slotmap::SlotMap<NodeId, ()> = slotmap::SlotMap::new();
sm.insert(())
}
struct CtxProbe;
impl Widget for CtxProbe {
fn render(
&self,
_console: &rich_rs::Console,
_options: &rich_rs::ConsoleOptions,
) -> rich_rs::Segments {
rich_rs::Segments::new()
}
}
#[test]
fn node_id_returns_dispatch_recipient() {
let w = CtxProbe;
assert_eq!(w.node_id(), NodeId::default());
let id = make_node_id();
let _guard = set_dispatch_recipient(id);
assert_eq!(w.node_id(), id);
assert_ne!(id, NodeId::default());
}
#[test]
fn render_styled_dyn_obj_sets_dispatch_context_and_restores() {
let console = rich_rs::Console::new();
let mut opts = rich_rs::ConsoleOptions::default();
opts.size = (10, 1);
opts.max_width = 10;
opts.max_height = 1;
let mut sm: slotmap::SlotMap<NodeId, ()> = slotmap::SlotMap::new();
let id_a = sm.insert(());
let id_b = sm.insert(());
assert_ne!(id_a, id_b);
let _outer = set_dispatch_recipient(id_a);
assert_eq!(dispatch_recipient(), Some(id_a));
let widget_b = CtxProbe;
let _ = widget_b.render_styled_dyn_obj(&console, &opts, None, id_b);
assert_eq!(
dispatch_recipient(),
Some(id_a),
"dispatch context must be restored after sibling render"
);
}
fn segments_text(segments: &Segments) -> String {
segments
.iter()
.map(|seg| seg.text.to_string())
.collect::<String>()
}
struct DefaultLineProbe;
impl Widget for DefaultLineProbe {
fn render(
&self,
_console: &rich_rs::Console,
_options: &rich_rs::ConsoleOptions,
) -> rich_rs::Segments {
vec![rich_rs::Segment::new("alpha\nbeta".to_string())].into()
}
}
#[test]
fn render_line_default_extracts_requested_row() {
let widget = DefaultLineProbe;
let console = rich_rs::Console::new();
let mut options = rich_rs::ConsoleOptions::default();
options.size = (16, 2);
options.max_width = 16;
options.max_height = 2;
let line0 = widget.render_line(0, &console, &options);
let line1 = widget.render_line(1, &console, &options);
assert_eq!(segments_text(&line0).trim_end(), "alpha");
assert_eq!(segments_text(&line1).trim_end(), "beta");
}
struct CustomLineProbe {
calls: Arc<AtomicUsize>,
}
impl Widget for CustomLineProbe {
fn render(
&self,
_console: &rich_rs::Console,
_options: &rich_rs::ConsoleOptions,
) -> rich_rs::Segments {
rich_rs::Segments::new()
}
fn render_line(
&self,
y: usize,
_console: &rich_rs::Console,
_options: &rich_rs::ConsoleOptions,
) -> rich_rs::Segments {
self.calls.fetch_add(1, Ordering::SeqCst);
vec![rich_rs::Segment::new(format!("line-{y}"))].into()
}
}
#[test]
fn render_lines_default_delegates_to_render_line() {
let calls = Arc::new(AtomicUsize::new(0));
let widget = CustomLineProbe {
calls: calls.clone(),
};
let console = rich_rs::Console::new();
let mut options = rich_rs::ConsoleOptions::default();
options.size = (16, 3);
options.max_width = 16;
options.max_height = 3;
let lines = widget.render_lines(2, 3, &console, &options);
assert_eq!(calls.load(Ordering::SeqCst), 3);
assert_eq!(segments_text(&lines[0]), "line-2");
assert_eq!(segments_text(&lines[1]), "line-3");
assert_eq!(segments_text(&lines[2]), "line-4");
}
}