rdom-tui 0.5.0

Terminal rendering layer for rdom-core — flexbox layout, TUI styles, key/mouse events. Use rdom-core directly for headless DOM manipulation.
Documentation
//! `impl TuiAccessorsMut for NodeMut` — write-side accessor methods
//! on `NodeMut<'a, TuiExt>`. Trait declaration lives in `super`
//! (mod.rs); private helpers live in `super::helpers`.

use rdom_core::NodeId;

use super::TuiAccessorsMut;
use super::helpers::{read_scroll_x, read_scroll_y, set_select_value, write_boolean_attribute};
use crate::node::install_text_content;
use crate::runtime::smooth_scroll::{ScrollIntoViewOptions, ScrollToOptions, perform_scroll};
use crate::{Result, TuiExt};

impl<'a> TuiAccessorsMut<'a> for rdom_core::NodeMut<'a, TuiExt> {
    fn set_value(&mut self, value: impl Into<String>) -> Result<()> {
        let tag = match self.as_ref().tag_name() {
            Some(t) => t.to_string(),
            None => return Ok(()),
        };
        let new_value = value.into();
        let id = self.id();
        let dom = self.dom_mut();
        match tag.as_str() {
            "input" => {
                if crate::node::is_text_input(dom, id) {
                    crate::runtime::builtins::input::set_value(dom, id, &new_value);
                } else {
                    // A range keeps its authored default; for the other
                    // types the attribute *is* the default (no-op).
                    crate::runtime::builtins::input::note_default_value(dom, id);
                    dom.set_attribute(id, "value", &new_value)?;
                }
                Ok(())
            }
            "textarea" => {
                crate::runtime::builtins::input::note_default_value(dom, id);
                crate::runtime::builtins::input::clear_user_edited(dom, id);
                install_text_content(dom, id, &new_value)
            }
            "select" => set_select_value(dom, id, &new_value),
            _ => Ok(()),
        }
    }

    fn set_checked(&mut self, value: bool) -> Result<()> {
        if self.as_ref().tag_name() != Some("input") {
            return Ok(());
        }
        let id = self.id();
        if crate::runtime::builtins::toggle::is_toggle(self.as_ref().dom(), id) {
            crate::runtime::builtins::toggle::note_default_checked(self.dom_mut(), id);
        }
        write_boolean_attribute(self, "checked", value)
    }

    fn set_default_value(&mut self, value: impl Into<String>) -> Result<()> {
        let value = value.into();
        let id = self.id();
        let dom = self.dom_mut();
        if crate::runtime::builtins::input::keeps_default_value(dom, id) {
            if let Some(ext) = dom.node_mut(id).ext_mut() {
                ext.default_value = Some(value);
            }
            return Ok(());
        }
        if dom.node(id).tag_name() == Some("input") {
            // Value mode "default" / "default/on": the attribute is both.
            dom.set_attribute(id, "value", &value)?;
        }
        Ok(())
    }

    fn set_default_checked(&mut self, value: bool) -> Result<()> {
        let id = self.id();
        let dom = self.dom_mut();
        if crate::runtime::builtins::toggle::is_toggle(dom, id)
            && let Some(ext) = dom.node_mut(id).ext_mut()
        {
            ext.default_checked = Some(value);
        }
        Ok(())
    }

    fn set_default_selected(&mut self, value: bool) -> Result<()> {
        let id = self.id();
        let dom = self.dom_mut();
        if dom.node(id).tag_name() == Some("option")
            && let Some(ext) = dom.node_mut(id).ext_mut()
        {
            ext.default_selected = Some(value);
        }
        Ok(())
    }

    fn set_indeterminate(&mut self, value: bool) -> Result<()> {
        if self.as_ref().tag_name() != Some("input") {
            return Ok(());
        }
        write_boolean_attribute(self, "indeterminate", value)
    }

    fn set_disabled(&mut self, value: bool) -> Result<()> {
        if !matches!(
            self.as_ref().tag_name(),
            Some("button" | "input" | "select" | "textarea" | "option" | "optgroup" | "fieldset")
        ) {
            return Ok(());
        }
        write_boolean_attribute(self, "disabled", value)
    }

    fn set_read_only(&mut self, value: bool) -> Result<()> {
        if !matches!(self.as_ref().tag_name(), Some("input" | "textarea")) {
            return Ok(());
        }
        write_boolean_attribute(self, "readonly", value)
    }

    fn set_inert(&mut self, value: bool) -> Result<()> {
        // `inert` is an HTMLElement-level attribute — applies to
        // every element with no tag gate.
        if self.as_ref().tag_name().is_none() {
            return Ok(());
        }
        write_boolean_attribute(self, "inert", value)
    }

    fn focus(&mut self) {
        let id = self.id();
        let dom = self.dom_mut();
        if !crate::runtime::focus::tabindex::is_focusable(dom, id) {
            return;
        }
        crate::runtime::focus::focus_node(dom, Some(id));
    }

    fn blur(&mut self) {
        let id = self.id();
        let dom = self.dom_mut();
        if dom.focused() != Some(id) {
            return;
        }
        crate::runtime::focus::focus_node(dom, None);
    }

    fn click(&mut self) {
        use rdom_core::{EventDetail, MouseButton, MouseDetail};
        let id = self.id();
        let dom = self.dom_mut();
        let mut event = rdom_core::Event::new("click").with_synthetic(true);
        event.detail = EventDetail::Mouse(MouseDetail::new(MouseButton::Left, 0, 0));
        let mut tui = crate::TuiEvent { event };
        crate::tui_event::dispatch_to_live(dom, id, &mut tui);
    }

    fn set_scroll_top(&mut self, value: i32) -> Result<()> {
        self.scroll_with(ScrollToOptions {
            top: Some(value),
            ..ScrollToOptions::default()
        })
    }

    fn set_scroll_left(&mut self, value: i32) -> Result<()> {
        self.scroll_with(ScrollToOptions {
            left: Some(value),
            ..ScrollToOptions::default()
        })
    }

    fn scroll_to(&mut self, x: i32, y: i32) -> Result<()> {
        self.scroll_with(ScrollToOptions {
            left: Some(x),
            top: Some(y),
            ..ScrollToOptions::default()
        })
    }

    fn scroll_by(&mut self, dx: i32, dy: i32) -> Result<()> {
        self.scroll_by_with(ScrollToOptions {
            left: Some(dx),
            top: Some(dy),
            ..ScrollToOptions::default()
        })
    }

    fn scroll_with(&mut self, options: ScrollToOptions) -> Result<()> {
        let id = self.id();
        let dom = self.dom_mut();
        let x = options.left.unwrap_or_else(|| read_scroll_x(dom, id));
        let y = options.top.unwrap_or_else(|| read_scroll_y(dom, id));
        perform_scroll(dom, id, x, y, options.behavior);
        Ok(())
    }

    fn scroll_by_with(&mut self, options: ScrollToOptions) -> Result<()> {
        let id = self.id();
        let dom = self.dom_mut();
        let x = read_scroll_x(dom, id).saturating_add(options.left.unwrap_or(0));
        let y = read_scroll_y(dom, id).saturating_add(options.top.unwrap_or(0));
        perform_scroll(dom, id, x, y, options.behavior);
        Ok(())
    }

    fn scroll_into_view(&mut self) -> Result<()> {
        self.scroll_into_view_with(ScrollIntoViewOptions::new())
    }

    fn scroll_into_view_with(&mut self, options: ScrollIntoViewOptions) -> Result<()> {
        let id = self.id();
        crate::runtime::scrollbar::scroll_element_into_view(self.dom_mut(), id, options);
        Ok(())
    }

    fn style_mut(&mut self) -> Option<crate::cssom::StyleDeclarationMut<'_>> {
        // Non-element nodes have no inline style — no-op.
        self.as_ref().tag_name()?;
        let id = self.id();
        let dom = self.dom_mut();
        Some(crate::cssom::StyleDeclarationMut::new(dom.node_mut(id)))
    }

    fn set_details_open(&mut self, value: bool) -> Result<()> {
        if self.as_ref().tag_name() != Some("details") {
            return Ok(());
        }
        write_boolean_attribute(self, "open", value)
    }

    fn set_dialog_return_value(&mut self, value: impl Into<String>) -> Result<()> {
        if self.as_ref().tag_name() != Some("dialog") {
            return Ok(());
        }
        let id = self.id();
        let dom = self.dom_mut();
        let s = value.into();
        crate::runtime::builtins::dialog::set_return_value(dom, id, &s);
        Ok(())
    }

    fn form_request_submit(
        &mut self,
        submitter: Option<NodeId>,
    ) -> Result<crate::runtime::builtins::form::SubmitOutcome> {
        if self.as_ref().tag_name() != Some("form") {
            return Ok(crate::runtime::builtins::form::SubmitOutcome::NotAForm);
        }
        let id = self.id();
        let dom = self.dom_mut();
        crate::runtime::builtins::form::request_submit(dom, id, submitter)
    }

    fn check_validity(&mut self) -> bool {
        let id = self.id();
        crate::runtime::builtins::validation::check_validity(self.dom_mut(), id)
    }

    fn report_validity(&mut self) -> bool {
        let id = self.id();
        crate::runtime::builtins::validation::report_validity(self.dom_mut(), id)
    }

    fn set_custom_validity(&mut self, message: &str) -> Result<()> {
        if !self
            .as_ref()
            .tag_name()
            .is_some_and(super::read_ref::has_validity)
        {
            return Ok(());
        }
        let id = self.id();
        crate::runtime::builtins::validation::set_custom_validity(self.dom_mut(), id, message);
        Ok(())
    }
}