声明式 UI 宏

在前面的章节中,我们已经系统梳理了 Ratatui Kit 的组件化、状态管理、Hook 系统、事件处理和 Context 机制。这些能力为终端 UI 的高效开发打下了坚实基础。但随着组件树变得越来越复杂,手动嵌套和属性传递会让代码变得冗长且难以维护。如何像前端那样用声明式的方式描述 UI?本节聚焦 Ratatui Kit 的声明式 UI 宏设计,介绍其设计理念、实现方式与实际用法。

1. 为什么需要声明式 UI 宏?

在没有宏的情况下,我们需要手动构造嵌套的 Element 结构体,每一层都要写 key、props、children,代码量大且嵌套不直观。例如:

let element = Element::<View> {
    key: ElementKey::new("root"),
    props: ViewProps {
        children: vec![
            Element::<View> {
                key: ElementKey::new("header"),
                props: ViewProps {
                    children: vec![
                        Element::<Text> {
                            key: ElementKey::new("title"),
                            props: TextProps {
                                text: "Welcome to the Counter App",
                                style: Style::default().bold().light_blue(),
                                alignment: ratatui::layout::Alignment::Center,
                            },
                        }
                        .into(),
                    ],
                    height: Constraint::Length(1),
                    ..Default::default()
                },
            }
            .into(),
            Element::<View> {
                key: ElementKey::new("body"),
                props: ViewProps {
                    children: vec![
                        Element::<Text> {
                            key: ElementKey::new("number"),
                            props: TextProps {
                                text: counter_text.as_str(),
                                style: Style::default().light_green(),
                                alignment: ratatui::layout::Alignment::Center,
                            },
                        }
                        .into(),
                    ],
                    height: Constraint::Fill(1),
                    ..Default::default()
                },
            }
            .into(),
            Element::<View> {
                key: ElementKey::new("footer"),
                props: ViewProps {
                    children: vec![
                    Element::<Text> {
                        key: ElementKey::new("info"),
                        props: TextProps {
                            text: "Press q or Ctrl+C to quit, + to increase, - to decrease",
                            style: Style::default().yellow(),
                            alignment: ratatui::layout::Alignment::Center,
                        },
                    }
                    .into(),
                ],
                    height: Constraint::Length(1),
                    ..Default::default()
                },
            }
            .into(),
        ],
        flex_direction: Direction::Vertical,
        gap: 3,
        ..Default::default()
    },
};

这段代码虽然功能完整,但可读性较差,尤其是嵌套的 Element 结构让人难以一目了然。为了解决这个问题,我们引入了 element! 宏。

2. element! 宏的设计与用法

element! 宏让 UI 结构声明更直观、简洁,支持如下用法:

  • 只写类型名,创建无属性元素:

    element!(View)
  • 类型名+括号属性,设置属性:

    element!(View(width: 80, height: 24))
  • 类型名+花括号,嵌套子元素:

    element! {
        View {
            Text(content: "Hello")
        }
    }
  • 使用 #() 块插入任意 Rust 表达式(如迭代器、条件分支等)动态生成子元素:

    element! {
        View {
            #(if show_greeting {
                Some(element!(Text(content: "Hello, world!")))
            } else {
                None
            })
        }
    }
  • 支持为每个元素指定唯一 key,保证动态列表渲染时状态正确:

    element! {
        View {
            #(users.iter().map(|user| element! {
                View(key: user.id) {
                    Text(content: format!("Hello, {}!", user.name))
                }
            }))
        }
    }

3. 宏实现的核心思路

  • 解析类型、属性、子元素,支持递归嵌套。
  • 支持 #() 语法插入任意 Rust 表达式,便于条件渲染和批量生成。
  • 自动为每个元素生成唯一 key,或允许手动指定 key,保证动态 UI 状态一致性。
  • 子元素批量扩展通过 extend_with_elements trait 实现,兼容单个元素、AnyElement、迭代器等多种情况。

4. 关键代码拆解与实现细节

下面我们结合具体文件,拆解 element! 宏背后的核心实现代码,并简要说明每段代码的作用。

4.1 子元素批量扩展 trait

src/element/extend_with_elements.rs中,定义了 trait ExtendWithElements,用于批量扩展子元素。

use super::{AnyElement, Element, ElementType};

/// 一个 trait,用于将元素批量扩展(append)到目标集合中。
/// 主要用于支持多种元素类型(单个元素、AnyElement、迭代器等)统一扩展到目标集合,
/// 便于声明式 UI 宏灵活拼接元素。
pub trait ExtendWithElements<T> {
    /// 将自身的元素批量扩展到 dest 中。
    fn extend_with_elements<E: Extend<T>>(self, dest: &mut E);
}

/// 支持将单个 Element 扩展到目标集合。
impl<'a, T, U> ExtendWithElements<T> for Element<'a, U>
where
    U: ElementType + 'a,
    T: From<Element<'a, U>>,
{
    fn extend_with_elements<E: Extend<T>>(self, dest: &mut E) {
        // 将单个 Element 转换为 T 后扩展到目标集合
        dest.extend([self.into()]);
    }
}

/// 支持将单个 AnyElement 扩展到目标集合。
impl<'a> ExtendWithElements<AnyElement<'a>> for AnyElement<'a> {
    fn extend_with_elements<E: Extend<AnyElement<'a>>>(self, dest: &mut E) {
        // 直接扩展单个 AnyElement
        dest.extend([self]);
    }
}

/// 支持将迭代器中的元素批量扩展到目标集合。
impl<T, U, I> ExtendWithElements<T> for I
where
    T: From<U>,
    I: IntoIterator<Item = U>,
{
    fn extend_with_elements<E: Extend<T>>(self, dest: &mut E) {
        // 将迭代器中的每个元素转换为 T 后批量扩展到目标集合
        dest.extend(self.into_iter().map(|x| x.into()));
    }
}

/// 通用扩展函数,支持将任意实现 ExtendWithElements 的元素批量扩展到目标集合。
/// 主要用于声明式 UI 宏内部,统一处理单个元素、AnyElement、迭代器等多种情况。
pub fn extend_with_elements<T, U, E>(dest: &mut T, elements: U)
where
    U: ExtendWithElements<E>,
    T: Extend<E>,
{
    elements.extend_with_elements(dest);
}

4.2 宏解析与展开

ratatui-kit-macros/src/element.rs中,实现宏的核心解析和展开逻辑,负责把宏输入的嵌套结构、属性、#() 表达式等,转成最终的 Element 构造代码。

use quote::{ToTokens, quote};
use syn::{
    Expr, FieldValue, Lit, Member, Result, Token, TypePath, braced, parenthesized,
    parse::{Parse, ParseStream},
    punctuated::Punctuated,
    token::{Brace, Comma, Paren},
};
use uuid::Uuid;

/// 表示一个子元素,可以是嵌套的 ParsedElement 或表达式(如 #(...expr))。
enum ParsedElementChild {
    Element(ParsedElement), // 嵌套子元素
    Expr(Expr),             // 动态表达式
}

/// 解析后的 UI 元素结构,包含类型、属性和子元素。
pub struct ParsedElement {
    ty: TypePath,                         // 元素类型
    props: Punctuated<FieldValue, Comma>, // 属性列表
    children: Vec<ParsedElementChild>,    // 子元素列表
}

/// 支持从宏输入流中解析出 ParsedElement 结构。
impl Parse for ParsedElement {
    fn parse(input: ParseStream) -> Result<Self> {
        // 解析类型名
        let ty: TypePath = input.parse()?;

        // 解析属性 (可选)
        let props = if input.peek(Paren) {
            let props_input;
            parenthesized!(props_input in input);
            Punctuated::parse_terminated(&props_input)?
        } else {
            Punctuated::new()
        };

        // 解析子元素 (可选)
        let mut children = Vec::new();
        if input.peek(Brace) {
            let children_input;
            braced!(children_input in input);
            while !children_input.is_empty() {
                if children_input.peek(Token![#]) {
                    // 支持 #(...) 语法,插入表达式作为子节点
                    children_input.parse::<Token![#]>()?;
                    let child_input;
                    parenthesized!(child_input in children_input);
                    children.push(ParsedElementChild::Expr(child_input.parse()?));
                } else {
                    // 递归解析嵌套子元素
                    children.push(ParsedElementChild::Element(children_input.parse()?));
                }
            }
        }

        Ok(Self {
            props,
            ty,
            children,
        })
    }
}

/// 将 ParsedElement 转换为 TokenStream,实现宏展开时的代码生成。
impl ToTokens for ParsedElement {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let ty = &self.ty;

        // 生成唯一 key(如未指定 key 属性则自动生成)
        let decl_key = Uuid::new_v4().as_u128();

        // 查找 key 属性,如果有则用 (decl_key, key_expr),否则用 decl_key
        let key = self
            .props
            .iter()
            .find_map(|FieldValue { member, expr, .. }| match member {
                Member::Named(ident) if ident == "key" => Some(quote!((#decl_key, #expr))),
                _ => None,
            })
            .unwrap_or_else(|| quote!(#decl_key));

        // 生成属性赋值代码,支持百分比语法 sugar(如 50pct)
        let prop_assignments = self
            .props
            .iter()
            .filter_map(|FieldValue { member, expr, .. }| match member {
                Member::Named(ident) if ident == "key" => None, // key 属性不赋值到 props
                _ => Some(match expr {
                    Expr::Lit(lit) => match &lit.lit {
                        Lit::Int(lit) if lit.suffix() == "pct" => {
                            let value = lit.base10_parse::<f32>().unwrap();
                            quote!(_props.#member = ::ratatui_kit_principle::Percent(#value).into())
                        }
                        Lit::Float(lit) if lit.suffix() == "pct" => {
                            let value = lit.base10_parse::<f32>().unwrap();
                            quote!(_props.#member = ::ratatui_kit_principle::Percent(#value).into())
                        }
                        _ => quote!(_props.#member = (#expr).into()),
                    },
                    _ => quote!(_props.#member = (#expr).into()),
                }),
            })
            .collect::<Vec<_>>();

        // 生成子元素扩展代码,支持嵌套和 #(...) 表达式
        let set_children = if !self.children.is_empty() {
            let children = self.children.iter().map(|child| match child {
                ParsedElementChild::Element(child) => quote!(#child),
                ParsedElementChild::Expr(expr) => quote!(#expr),
            });
            Some(quote! {
                #(::ratatui_kit_principle::element::extend_with_elements(&mut _element.props.children, #children);)*
            })
        } else {
            None
        };

        // 生成最终 Element 构造代码
        tokens.extend(quote! {
            {
                type Props<'a> = <#ty as ::ratatui_kit_principle::element::ElementType>::Props<'a>;
                let mut _props: Props = Default::default();
                #(#prop_assignments;)*
                let mut _element = ::ratatui_kit_principle::element::Element::<#ty>{
                    key: ::ratatui_kit_principle::element::ElementKey::new(#key),
                    props: _props,
                };
                #set_children
                _element
            }
        });
    }
}

4.3 宏导出入口

然后在ratatui-kit-macros/src/lib.rs中添加宏导出入口:

#[proc_macro]
pub fn element(input: TokenStream) -> TokenStream {
    let element = syn::parse_macro_input!(input as element::ParsedElement);
    element.to_token_stream().into()
}

5. 使用 element! 宏后的效果

有了 element! 宏后,之前冗长的 UI 构建代码可以简化为:

let element = element! {
    View(flex_direction: Direction::Vertical,gap: 3,){
        View(height: Constraint::Length(1),){
            Text(
                text: "Welcome to the Counter App",
                style: Style::default().bold().light_blue(),
                alignment: ratatui::layout::Alignment::Center
            )
        }
        View(height: Constraint::Fill(1),){
            Text(
                text: counter_text.as_str(),
                style: Style::default().light_green(),
                alignment: ratatui::layout::Alignment::Center
            )
        }
        View(height: Constraint::Length(1),){
            Text(
                text: "Press q or Ctrl+C to quit, + to increase, - to decrease",
                style: Style::default().yellow(),
                alignment: ratatui::layout::Alignment::Center
            )
        }
    }
};

这样,我们就可以像写前端 JSX/Flutter 那样声明式地描述终端 UI 结构,代码更简洁、层次更清晰、维护更高效。

总结

element! 宏让写终端 UI 变得像写前端 JSX 或 Flutter 一样直观,声明和维护都很轻松。属性、嵌套、动态表达式、key 管理等能力一应俱全,复杂 UI 场景也能优雅应对。

需要说明的是,这其实只是 ratatui-kit 宏原理体系中的一小部分。比如 component! 宏等其它声明式能力,底层原理也大同小异:都是通过 Rust 的宏系统,把结构化的声明语法转换成模板代码,帮你省去重复劳动,让 UI 代码更聚焦于业务本身。

最后

本系列文章到此就结束了。希望通过对 Ratatui Kit 原理的深入剖析,能让你对这个库有更全面的理解,也能激发你在终端 UI 开发中的创造力。