use rdom_core::{NodeMut, NodeRef, NodeType};
use crate::ext::TuiExt;
use crate::layout::{Border, Direction, LayoutRect, Overflow, Padding, Size};
use crate::style::{ComputedStyle, TuiStyle, Value};
pub trait TuiNodeExt<'a> {
fn tui_ext(&self) -> Option<&'a TuiExt>;
fn width(&self) -> Option<Size> {
self.inline_style()
.and_then(|s| s.width.as_ref())
.and_then(|v| v.as_specified().cloned())
}
fn height(&self) -> Option<Size> {
self.inline_style()
.and_then(|s| s.height.as_ref())
.and_then(|v| v.as_specified().cloned())
}
fn direction(&self) -> Option<Direction> {
self.inline_style()
.and_then(|s| s.direction.as_ref())
.and_then(|v| v.as_specified().copied())
}
fn padding(&self) -> Option<Padding> {
self.inline_style()
.and_then(|s| s.padding.as_ref())
.and_then(|v| v.as_specified().cloned())
}
fn border(&self) -> Option<Border> {
self.inline_style()
.and_then(|s| s.border.as_ref())
.and_then(|v| v.as_specified().copied())
}
fn gap(&self) -> Option<u16> {
self.inline_style()
.and_then(|s| s.gap.as_ref())
.and_then(|v| v.as_specified())
.and_then(|g| g.as_cells())
}
fn overflow(&self) -> Option<Overflow> {
self.inline_style()
.and_then(|s| s.overflow_x.as_ref())
.and_then(|v| v.as_specified().copied())
}
fn inline_style(&self) -> Option<&'a TuiStyle> {
self.tui_ext().map(TuiExt::inline_style_or_empty)
}
fn layout_rect(&self) -> Option<LayoutRect> {
self.tui_ext().map(|e| e.layout)
}
fn content_layout_rect(&self) -> Option<LayoutRect> {
self.tui_ext().map(|e| e.content_layout)
}
fn computed(&self) -> Option<&'a ComputedStyle> {
self.tui_ext().and_then(|e| e.computed.as_deref())
}
fn computed_rc(&self) -> Option<std::rc::Rc<ComputedStyle>> {
self.tui_ext().and_then(|e| e.computed.clone())
}
fn computed_or_initial(&self) -> ComputedStyle {
self.computed()
.cloned()
.unwrap_or_else(ComputedStyle::initial)
}
fn computed_before(&self) -> Option<&'a ComputedStyle> {
self.tui_ext().and_then(|e| e.computed_before.as_deref())
}
fn computed_after(&self) -> Option<&'a ComputedStyle> {
self.tui_ext().and_then(|e| e.computed_after.as_deref())
}
fn is_style_dirty(&self) -> bool {
self.tui_ext().is_some_and(|e| e.style_dirty)
}
fn is_layout_dirty(&self) -> bool {
self.tui_ext().is_some_and(|e| e.layout_dirty)
}
fn is_editable(&self) -> bool;
}
pub(crate) fn is_text_input<Ext>(dom: &rdom_core::Dom<Ext>, id: rdom_core::NodeId) -> bool {
use rdom_core::InputTypeState as T;
matches!(
dom.input_type_state(id),
Some(T::Text | T::Search | T::Tel | T::Url | T::Email | T::Password | T::Number)
)
}
pub(crate) fn is_rendered(dom: &crate::TuiDom, id: rdom_core::NodeId) -> bool {
let mut cur = Some(id);
while let Some(n) = cur {
let node = dom.node(n);
if node
.computed()
.is_some_and(|c| c.display == crate::layout::Display::None)
{
return false;
}
cur = node.parent_node().map(|p| p.id());
}
true
}
pub(crate) fn child_text<Ext>(dom: &rdom_core::Dom<Ext>, id: rdom_core::NodeId) -> String {
let mut out = String::new();
for child in dom.node(id).child_nodes() {
if child.node_type() == NodeType::Text
&& let Some(s) = child.node_value()
{
out.push_str(s);
}
}
out
}
impl<'a> TuiNodeExt<'a> for NodeRef<'a, TuiExt> {
fn tui_ext(&self) -> Option<&'a TuiExt> {
self.ext()
}
fn is_editable(&self) -> bool {
if self.dom().is_actually_disabled(self.id()) {
return false;
}
if self
.dom()
.content_editable_state(self.id())
.is_some_and(rdom_core::ContentEditableState::is_editing_host)
{
return true;
}
match self.tag_name() {
Some("textarea") => true,
Some("input") => is_text_input(self.dom(), self.id()),
_ => false,
}
}
}
pub fn nearest_editable_ancestor(
dom: &rdom_core::Dom<TuiExt>,
node_id: rdom_core::NodeId,
) -> Option<rdom_core::NodeId> {
let mut cur = Some(node_id);
while let Some(id) = cur {
if dom.node(id).is_editable() {
return Some(id);
}
if dom.content_editable_state(id) == Some(rdom_core::ContentEditableState::False) {
return None;
}
cur = dom.node(id).parent_node().map(|p| p.id());
}
None
}
pub fn is_descendant_or_self(
dom: &rdom_core::Dom<TuiExt>,
node: rdom_core::NodeId,
ancestor: rdom_core::NodeId,
) -> bool {
let mut cur = Some(node);
while let Some(n) = cur {
if n == ancestor {
return true;
}
cur = dom.node(n).parent_node().map(|p| p.id());
}
false
}
pub fn first_text_descendant(
dom: &rdom_core::Dom<TuiExt>,
root: rdom_core::NodeId,
) -> Option<rdom_core::NodeId> {
if dom.node(root).node_type() == NodeType::Text {
return Some(root);
}
for child in dom.node(root).child_nodes() {
if let Some(found) = first_text_descendant(dom, child.id()) {
return Some(found);
}
}
None
}
pub fn last_text_descendant(
dom: &rdom_core::Dom<TuiExt>,
root: rdom_core::NodeId,
) -> Option<rdom_core::NodeId> {
if dom.node(root).node_type() == NodeType::Text {
return Some(root);
}
let kids: Vec<rdom_core::NodeId> = dom.node(root).child_nodes().map(|c| c.id()).collect();
for child in kids.into_iter().rev() {
if let Some(found) = last_text_descendant(dom, child) {
return Some(found);
}
}
None
}
pub fn text_len(dom: &rdom_core::Dom<TuiExt>, id: rdom_core::NodeId) -> usize {
dom.node(id).node_value().map(|s| s.len()).unwrap_or(0)
}
pub(crate) fn install_text_content(
dom: &mut rdom_core::Dom<TuiExt>,
id: rdom_core::NodeId,
text: &str,
) -> crate::Result<()> {
let existing: Vec<rdom_core::NodeId> = dom.node(id).child_nodes().map(|c| c.id()).collect();
for child in existing {
dom.remove_child(id, child)?;
}
let text_node = dom.create_text_node(text);
dom.append_child(id, text_node)?;
Ok(())
}
pub trait TuiNodeMutExt<'a> {
fn tui_ext_mut(&mut self) -> Option<&mut TuiExt>;
fn write_inline_style(&mut self, f: impl FnOnce(&mut TuiStyle)) {
if let Some(e) = self.tui_ext_mut() {
f(e.inline_style_mut());
e.style_dirty = true;
}
}
fn set_width(&mut self, w: Size) -> &mut Self {
self.write_inline_style(|s| s.width = Some(Value::Specified(w)));
self
}
fn set_height(&mut self, h: Size) -> &mut Self {
self.write_inline_style(|s| s.height = Some(Value::Specified(h)));
self
}
fn set_min_width(&mut self, v: Option<rdom_style::layout::MinSize>) -> &mut Self {
self.write_inline_style(|s| s.min_width = v.map(Value::Specified));
self
}
fn set_max_width(&mut self, v: Option<u16>) -> &mut Self {
self.write_inline_style(|s| s.max_width = v.map(Value::Specified));
self
}
fn set_min_height(&mut self, v: Option<rdom_style::layout::MinSize>) -> &mut Self {
self.write_inline_style(|s| s.min_height = v.map(Value::Specified));
self
}
fn set_max_height(&mut self, v: Option<u16>) -> &mut Self {
self.write_inline_style(|s| s.max_height = v.map(Value::Specified));
self
}
fn set_direction(&mut self, d: Direction) -> &mut Self {
self.write_inline_style(|s| s.direction = Some(Value::Specified(d)));
self
}
fn set_padding(&mut self, p: Padding) -> &mut Self {
self.write_inline_style(|s| s.padding = Some(Value::Specified(p)));
self
}
fn set_border(&mut self, b: Border) -> &mut Self {
self.write_inline_style(|s| s.border = Some(Value::Specified(b)));
self
}
fn set_gap(&mut self, g: u16) -> &mut Self {
self.write_inline_style(|s| {
s.gap = Some(Value::Specified(crate::layout::GapValue::Cells(g)))
});
self
}
fn set_overflow(&mut self, o: Overflow) -> &mut Self {
self.write_inline_style(|s| {
s.overflow_x = Some(Value::Specified(o));
s.overflow_y = Some(Value::Specified(o));
});
self
}
fn set_inline_style(&mut self, style: TuiStyle) -> &mut Self {
self.write_inline_style(|s| *s = style);
self
}
fn set_before_content(&mut self, text: impl Into<String>) -> &mut Self {
if let Some(e) = self.tui_ext_mut() {
e.before_content = Some(text.into());
}
self
}
fn clear_before_content(&mut self) -> &mut Self {
if let Some(e) = self.tui_ext_mut() {
e.before_content = None;
}
self
}
fn set_after_content(&mut self, text: impl Into<String>) -> &mut Self {
if let Some(e) = self.tui_ext_mut() {
e.after_content = Some(text.into());
}
self
}
fn clear_after_content(&mut self) -> &mut Self {
if let Some(e) = self.tui_ext_mut() {
e.after_content = None;
}
self
}
fn set_scroll(&mut self, x: usize, y: usize) -> &mut Self {
if let Some(e) = self.tui_ext_mut() {
e.scroll_x = x;
e.scroll_y = y;
}
self
}
}
impl<'a> TuiNodeMutExt<'a> for NodeMut<'a, TuiExt> {
fn tui_ext_mut(&mut self) -> Option<&mut TuiExt> {
self.ext_mut()
}
fn write_inline_style(&mut self, f: impl FnOnce(&mut TuiStyle)) {
let id = self.id();
crate::cssom::inline::write_inline_style(self.dom_mut(), id, f);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::TuiDom;
use crate::style::Color;
#[test]
fn builder_chain_sets_fields() {
let mut dom: TuiDom = TuiDom::new();
let div = dom.create_element("div");
dom.node_mut(div)
.set_width(Size::Fixed(40))
.set_height(Size::Flex(1))
.set_padding(Padding::symmetric(2, 1))
.set_border(Border::single())
.set_gap(1)
.set_overflow(Overflow::Hidden)
.set_direction(Direction::Row);
let n = dom.node(div);
assert_eq!(n.width(), Some(Size::Fixed(40)));
assert_eq!(n.height(), Some(Size::Flex(1)));
assert_eq!(n.padding(), Some(Padding::symmetric(2, 1)));
assert_eq!(n.border(), Some(Border::single()));
assert_eq!(n.gap(), Some(1));
assert_eq!(n.overflow(), Some(Overflow::Hidden));
assert_eq!(n.direction(), Some(Direction::Row));
}
#[test]
fn min_max_constraints() {
use rdom_style::layout::MinSize;
let mut dom: TuiDom = TuiDom::new();
let div = dom.create_element("div");
dom.node_mut(div)
.set_min_width(Some(MinSize::Cells(10)))
.set_max_width(Some(100))
.set_min_height(Some(MinSize::Cells(5)))
.set_max_height(Some(50));
let e = dom.node(div).tui_ext().unwrap();
use crate::style::Value;
assert_eq!(
e.inline_style_or_empty().min_width,
Some(Value::Specified(MinSize::Cells(10)))
);
assert_eq!(
e.inline_style_or_empty().max_width,
Some(Value::Specified(100))
);
assert_eq!(
e.inline_style_or_empty().min_height,
Some(Value::Specified(MinSize::Cells(5)))
);
assert_eq!(
e.inline_style_or_empty().max_height,
Some(Value::Specified(50))
);
}
#[test]
fn inline_style_settable() {
let mut dom: TuiDom = TuiDom::new();
let div = dom.create_element("div");
dom.node_mut(div)
.set_inline_style(TuiStyle::new().fg(Color::Rgb(255, 0, 0)).bold(true));
let n = dom.node(div);
let s = n.inline_style().unwrap();
use crate::style::{TuiColor, Value};
assert_eq!(
s.fg,
Some(Value::Specified(TuiColor::Literal(Color::Rgb(255, 0, 0))))
);
assert_eq!(s.bold, Some(Value::Specified(true)));
}
#[test]
fn before_after_content_setters() {
let mut dom: TuiDom = TuiDom::new();
let div = dom.create_element("div");
dom.node_mut(div)
.set_before_content("▾ ")
.set_after_content(" ←");
let e = dom.node(div).tui_ext().unwrap();
assert_eq!(e.before_content.as_deref(), Some("▾ "));
assert_eq!(e.after_content.as_deref(), Some(" ←"));
dom.node_mut(div).clear_before_content();
assert!(dom.node(div).ext().unwrap().before_content.is_none());
}
#[test]
fn setters_noop_on_non_element() {
let mut dom: TuiDom = TuiDom::new();
let t = dom.create_text_node("hi");
dom.node_mut(t)
.set_width(Size::Fixed(5))
.set_border(Border::single());
assert!(dom.node(t).tui_ext().is_none());
}
#[test]
fn scroll_setter() {
let mut dom: TuiDom = TuiDom::new();
let div = dom.create_element("div");
dom.node_mut(div).set_scroll(12, 34);
let e = dom.node(div).tui_ext().unwrap();
assert_eq!(e.scroll_x, 12);
assert_eq!(e.scroll_y, 34);
}
}