文本与文本输入
界面上的大部分信息都靠文字传达。Tessera 用一个组件显示文本,用 Material 输入框收集文本,两者的字体和颜色都来自主题,同一界面上的标签因此保持一致。
本页示例都默认放在应用的 material_theme() 下运行,根组件的配置见 Material 3 主题。
显示文本
text() 用统一的一种样式绘制一段字符串。从主题的排版层级里选一个角色,这样主题变化时,标题和正文之间的比例仍然协调:
use tessera_components::{column::column, text::text, theme::MaterialTheme};
use tessera_ui::{tessera, use_context};
#[tessera]
fn article_header() {
let theme = use_context::<MaterialTheme>()
.expect("article_header requires material_theme")
.get();
column().children(move || {
text("Release notes").style(theme.typography.headline_small);
text("Updated today")
.style(theme.typography.body_medium)
.color(theme.color_scheme.on_surface_variant);
});
}没有设置 style 时,text 先使用父级继承下来的 TextStyle(例如按钮会为自己的标签提供一份),没有的话再用主题的 body_large。颜色也按同样的顺序决定:先看显式的 color,再看继承的 ContentColor,最后是主题的 on_surface。所以放在按钮或填充表面里的文字,通常会自动用上合适的颜色。
设置文本样式
排版层级包含 display、headline、title、body 和 label 五类角色,每类有大、中、小三个尺寸。TextStyle 包含字号、可选的行高、字重、字间距和可选的字体族。某段内容需要和所属角色略有不同时,以该角色为基础,只改不同的部分:
use tessera_components::{
text::text,
theme::{MaterialTheme, TextStyle},
};
use tessera_ui::{tessera, use_context};
#[tessera]
fn report_title() {
let theme = use_context::<MaterialTheme>()
.expect("report_title requires material_theme")
.get();
text("Quarterly report for the northern region")
.style(TextStyle {
font_weight: 600,
..theme.typography.title_medium
})
.single_line(true);
}single_line(true) 让文本保持一行,放不下的部分以省略号结尾;不设置时,文本会按父级允许的宽度换行。没有指定行高时,默认取字号的 1.2 倍。临时覆盖某一项,也可以用 text 的 size 和 line_height。
如果许多组件都要跟着变,就改一次主题排版;theme.typography.emphasized() 可以生成一套用于强调内容的更醒目的层级。text 只绘制一种样式,一句话里要混用不同字重或颜色时,需要把几个 text 排在一起。字体注册和回退见资源。
文本也会提供给无障碍服务:默认以显示的内容作为标签,角色为 label。朗读的名称需要和显示的文字不同时,用 accessibility_label 或 accessibility_description 覆盖。这些元数据的用途见语义。
接收文本输入
text_field() 是 Material 风格的输入框,编辑、选择、光标、输入法和剪贴板都已经替你处理好。通过构建器可以设置标签、占位文字和几种预设:
use tessera_components::{
column::column,
text_field::{TextFieldLineLimit, text_field},
};
use tessera_ui::tessera;
#[tessera]
fn sign_in_form() {
column().children(|| {
text_field().label("Email").placeholder("name@example.com");
text_field().label("Password").secure();
text_field()
.label("Notes")
.outlined()
.line_limit(TextFieldLineLimit::MultiLine);
});
}输入框默认是填充样式、单行输入。outlined() 切换为描边样式,TextFieldLineLimit::MultiLine 允许多行。前后图标、前缀、后缀和错误状态等也都有对应的设置方法,完整列表见输入框参考。
read_only 和 enabled 代表两种不同的策略:只读输入框的内容仍然可用,只是不接受编辑;禁用的输入框则完全不接受用户输入。on_submit 处理单行输入框的提交。
响应文本变化
on_change 接收候选的 String,并且返回最终接受的 String,而不是 ()。输入框保留的正是这个返回值,所以即使回调只是把文本记下来,也要把值返回:
let query = remember(String::new);
text_field().label("Search").on_change(move |value: String| {
query.set(value.clone());
value
});编辑规则应该写在 input_transform 里,它在 on_change 之前执行。下面的输入框只保留候选文本中的数字:
text_field()
.label("Verification code")
.input_transform(|value: String| value.chars().filter(char::is_ascii_digit).collect());这类规则都放在 input_transform 里。组件函数体每次重建都会运行,不适合用来修改用户输入的内容。
在代码中控制文本
如果你没有传入控制器,text_field() 会自己创建并持有一个。当应用动作需要读取或替换文本、或者把焦点移到编辑器上时,就自己保存一个 TextInputController:
use tessera_components::{
button::button, column::column, text::text, text_field::text_field,
text_input::TextInputController,
};
use tessera_ui::{Dp, remember, tessera};
#[tessera]
fn search_field() {
let editor = remember(|| TextInputController::new(Dp(16.0), Some(Dp(24.0))));
column().children(move || {
text_field().controller(editor).label("Search");
button()
.on_click(move || {
editor.with_mut(|editor| editor.set_text(""));
editor.with(|editor| editor.request_focus());
})
.child(|| text("Clear and focus"));
});
}用 editor.with(|editor| editor.text()) 读取当前内容。initial_text 只在内部控制器创建时提供一次初始文本,之后的重建会保留当前文本。传入外部控制器时,请直接初始化它。
输入法与剪贴板
编辑器已经处理了选择、IME 预编辑和已提交文本、光标定位以及剪贴板编辑。请在它的基础上处理文本输入,而不是靠按键拼接文本:一次按键不一定对应一个字符,输入法也可能改写用户尚未完成的组合文本。
TextFieldContextMenu 用来配置输入框的剪切、复制、粘贴和全选菜单。secure() 会遮盖显示内容、限制为单行,并换用不提供剪切和复制的上下文菜单策略。
注意
敏感数据仍需由应用自身保护。这些设置只改变输入框的显示内容和菜单选项。
应用异步编辑结果之前,先确认它仍然对应当前的内容和选择。比如剪切,要等剪贴板写入成功后再删除文本。获得焦点的剪贴板输入路径通过 ClipboardInput::write_text 支持这一点:只有平台确认成功,完成回调才会运行。普通的文本编辑仍然走现有的控制器和输入管线。
完全自定义的编辑器通过 ImeInput 接收组合输入,再通过 ImeSession 和 ImeRequest 发布文本、选择和输入配置。除了键盘处理器,它还需要键盘焦点、剪贴板处理和无障碍语义。原始输入 trait 见手势与交互,编辑器的约定见文本输入参考。