use crate::layout::{LayoutRect, Length, Padding, Position, Size, ZIndex};
use crate::render::inline::InlineLayout;
use crate::runtime::editing::EditorState;
use crate::style::{Color, ComputedStyle, TuiStyle};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PseudoLayout {
pub rect: LayoutRect,
pub position: Position,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StaticPosition {
pub x: i32,
pub y: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct MarginChainMemo {
pub containing_block_width: u16,
pub outer_top: Option<(i16, i16)>,
pub outer_bottom: Option<(i16, i16)>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct TypeaheadState {
pub buffer: String,
pub last: Option<std::time::Instant>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AnonymousIfc {
pub rect: LayoutRect,
pub inline_layout: InlineLayout,
pub child_range: (usize, usize),
}
#[derive(Debug, Clone, Default, PartialEq)]
#[non_exhaustive]
pub struct PresentationStyle {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub border_fg: Option<Color>,
pub width: Option<Size>,
pub height: Option<Size>,
pub padding: Option<Padding>,
pub gap: Option<u16>,
pub top: Option<Length>,
pub right: Option<Length>,
pub bottom: Option<Length>,
pub left: Option<Length>,
pub z_index: Option<ZIndex>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum StyleSlot {
#[default]
Host,
Before,
After,
}
impl StyleSlot {
pub fn pseudo_element(self) -> Option<&'static str> {
match self {
StyleSlot::Host => None,
StyleSlot::Before => Some("::before"),
StyleSlot::After => Some("::after"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PseudoSlot {
Before,
After,
}
impl From<PseudoSlot> for StyleSlot {
fn from(slot: PseudoSlot) -> Self {
match slot {
PseudoSlot::Before => StyleSlot::Before,
PseudoSlot::After => StyleSlot::After,
}
}
}
impl TuiExt {
pub fn presentation_for(&self, slot: StyleSlot) -> Option<&PresentationStyle> {
match slot {
StyleSlot::Host => self.presentation.as_deref(),
StyleSlot::Before => self.presentation_before.as_deref(),
StyleSlot::After => self.presentation_after.as_deref(),
}
}
pub(crate) fn presentation_for_mut(&mut self, slot: StyleSlot) -> &mut PresentationStyle {
self.presentation_slot(slot)
.get_or_insert_with(Default::default)
}
pub(crate) fn release_empty_presentation(&mut self, slot: StyleSlot) {
let boxed = self.presentation_slot(slot);
if boxed.as_deref().is_some_and(PresentationStyle::is_empty) {
*boxed = None;
}
}
fn presentation_slot(&mut self, slot: StyleSlot) -> &mut Option<Box<PresentationStyle>> {
match slot {
StyleSlot::Host => &mut self.presentation,
StyleSlot::Before => &mut self.presentation_before,
StyleSlot::After => &mut self.presentation_after,
}
}
pub fn inline_style_or_empty(&self) -> &TuiStyle {
static EMPTY: std::sync::LazyLock<TuiStyle> = std::sync::LazyLock::new(TuiStyle::default);
self.inline_style.as_deref().unwrap_or(&EMPTY)
}
pub fn inline_style_mut(&mut self) -> &mut TuiStyle {
self.inline_style.get_or_insert_with(Default::default)
}
pub fn set_inline_style(&mut self, style: TuiStyle) {
self.inline_style = (!style.is_empty()).then(|| Box::new(style));
}
}
impl PresentationStyle {
pub fn is_empty(&self) -> bool {
self.fg.is_none()
&& self.bg.is_none()
&& self.border_fg.is_none()
&& self.width.is_none()
&& self.height.is_none()
&& self.padding.is_none()
&& self.gap.is_none()
&& self.top.is_none()
&& self.right.is_none()
&& self.bottom.is_none()
&& self.left.is_none()
&& self.z_index.is_none()
}
pub fn clear(&mut self) {
*self = PresentationStyle::default();
}
}
#[derive(Debug, Default, PartialEq)]
pub struct TuiExt {
pub inline_style: Option<Box<TuiStyle>>,
pub before_content: Option<String>,
pub after_content: Option<String>,
pub table_used_width: Option<u16>,
pub scroll_x: usize,
pub scroll_y: usize,
pub(crate) scroll_state: Option<Box<crate::runtime::scrollbar::state::ScrollState>>,
pub scroll_content_width: usize,
pub scroll_content_height: usize,
pub layout: LayoutRect,
pub content_layout: LayoutRect,
pub static_position: Option<StaticPosition>,
pub before_layout: Option<PseudoLayout>,
pub after_layout: Option<PseudoLayout>,
pub tree_has_positioned_pseudo: bool,
pub tree_has_collapse: bool,
pub inline_layout: Option<InlineLayout>,
pub anonymous_blocks: Vec<AnonymousIfc>,
pub computed: Option<std::rc::Rc<ComputedStyle>>,
pub computed_prev: Option<std::rc::Rc<ComputedStyle>>,
pub presentation: Option<Box<PresentationStyle>>,
pub computed_before: Option<std::rc::Rc<ComputedStyle>>,
pub computed_after: Option<std::rc::Rc<ComputedStyle>>,
pub computed_before_prev: Option<std::rc::Rc<ComputedStyle>>,
pub computed_after_prev: Option<std::rc::Rc<ComputedStyle>>,
pub presentation_before: Option<Box<PresentationStyle>>,
pub presentation_after: Option<Box<PresentationStyle>>,
pub computed_backdrop: Option<std::rc::Rc<ComputedStyle>>,
pub computed_selection: Option<std::rc::Rc<ComputedStyle>>,
pub computed_scrollbar: Option<std::rc::Rc<ComputedStyle>>,
pub computed_scrollbar_thumb_vertical: Option<std::rc::Rc<ComputedStyle>>,
pub computed_scrollbar_thumb_horizontal: Option<std::rc::Rc<ComputedStyle>>,
pub style_dirty: bool,
pub layout_dirty: bool,
pub(crate) margin_chain: Option<MarginChainMemo>,
pub default_value: Option<String>,
pub default_checked: Option<bool>,
pub default_selected: Option<bool>,
pub(crate) caret_reveal_pending: bool,
pub(crate) caret_blink_off: bool,
pub editor_state: Option<Box<EditorState>>,
pub dialog_return_focus: Option<rdom_core::NodeId>,
pub typeahead: Option<Box<TypeaheadState>>,
pub canvas_paint: Option<crate::runtime::builtins::canvas::CanvasPaint>,
pub(crate) form_state: crate::runtime::builtins::form_state::FormControlSlot,
}
impl TuiExt {
pub fn new() -> Self {
Self::default()
}
}
impl Clone for TuiExt {
fn clone(&self) -> Self {
Self {
inline_style: self.inline_style.clone(),
before_content: self.before_content.clone(),
after_content: self.after_content.clone(),
default_value: self.default_value.clone(),
default_checked: self.default_checked,
default_selected: self.default_selected,
..Self::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Color;
#[test]
fn defaults_are_sensible() {
let ext = TuiExt::new();
assert!(ext.inline_style.is_none());
assert_eq!(ext.scroll_x, 0);
assert_eq!(ext.scroll_y, 0);
assert!(ext.inline_style.is_none());
assert!(ext.before_content.is_none());
assert!(ext.after_content.is_none());
assert_eq!(ext.layout, LayoutRect::default());
assert!(ext.computed.is_none());
assert!(ext.computed_before.is_none());
assert!(ext.computed_after.is_none());
assert!(!ext.style_dirty);
assert!(!ext.layout_dirty);
}
#[test]
fn clone_copies_author_inputs_and_resets_runtime_state() {
let mut dom: crate::TuiDom = crate::TuiDom::new();
let opener = dom.create_element("button");
let mut ext = TuiExt {
inline_style: Some(Box::new(
TuiStyle::new()
.fg(Color::Rgb(255, 0, 0))
.width(Size::Fixed(80))
.padding(Padding::all(2)),
)),
before_content: Some("▾ ".into()),
after_content: Some(" ←".into()),
default_value: Some("hi".into()),
default_checked: Some(true),
default_selected: Some(false),
scroll_x: 3,
scroll_y: 9,
scroll_state: Some(Box::default()),
caret_reveal_pending: true,
caret_blink_off: true,
style_dirty: true,
dialog_return_focus: Some(opener),
..Default::default()
};
{
let form = ext.form_state.get_mut();
form.custom_validity = "taken".into();
form.value_user_edited = true;
form.firing_submission_events = true;
}
let cloned = ext.clone();
assert_eq!(cloned.inline_style, ext.inline_style);
assert_eq!(cloned.before_content, ext.before_content);
assert_eq!(cloned.after_content, ext.after_content);
assert_eq!(cloned.default_value, ext.default_value);
assert_eq!(cloned.default_checked, ext.default_checked);
assert_eq!(cloned.default_selected, ext.default_selected);
let fresh = TuiExt {
inline_style: ext.inline_style.clone(),
before_content: ext.before_content.clone(),
after_content: ext.after_content.clone(),
default_value: ext.default_value.clone(),
default_checked: ext.default_checked,
default_selected: ext.default_selected,
..Default::default()
};
assert_eq!(cloned, fresh, "everything else is at its default");
assert!(cloned.form_state.get().is_none(), "no form state carried");
assert!(cloned.scroll_state.is_none(), "no scroll in flight");
}
#[test]
fn tui_ext_size_tripwire() {
const MAX: usize = 432;
let size = std::mem::size_of::<TuiExt>();
let computed = std::mem::size_of::<ComputedStyle>();
let inline = std::mem::size_of::<TuiStyle>();
let presentation = std::mem::size_of::<PresentationStyle>();
eprintln!(
"TuiExt {size} B; ComputedStyle {computed} B, TuiStyle {inline} B, \
PresentationStyle {presentation} B"
);
assert!(size <= MAX, "size_of::<TuiExt>() = {size}, bound {MAX}");
}
#[test]
fn partial_eq_works() {
let a = TuiExt {
inline_style: Some(Box::new(TuiStyle::new().width(Size::Fixed(10)))),
..Default::default()
};
let b = TuiExt {
inline_style: Some(Box::new(TuiStyle::new().width(Size::Fixed(10)))),
..Default::default()
};
let c = TuiExt {
inline_style: Some(Box::new(TuiStyle::new().width(Size::Fixed(11)))),
..Default::default()
};
assert_eq!(a, b);
assert_ne!(a, c);
}
}