# TextField

> 单行文本字段，支持掩码、EditMode、密码模式、可点击图标和浮动 caption。

Source: https://crossfly.dev/zh/docs/ui/widgets/textfield — CrossFly documentation. Full docs as one file: https://crossfly.dev/llms-full.txt

# TextField

`TextField` 是**单行**文本输入。在基类 [`TextInput`](/docs/ui/widgets/textinput) 之上，它增加了：**掩码**引擎（日期、货币、自定义）、**密码模式**（带显示/隐藏切换）、可点击的左/右图标、水平滚动和一个可选的**浮动 caption**。输入行为由 [`EditMode`](/docs/reference)（NORMAL/DATE/CURRENCY/NUMERIC/TIME/PASSWORD）定义，它同时也选择默认键盘。

> 🟡 · **包：** `dev.crossfly.ui` · **继承：** `TextInput` · **样式：** `TextFieldStyle`（default 与 focused 各异）

## 何时使用
- 任何**单行**输入：姓名、邮箱、密码、数字/货币值、日期、时间。
- 带掩码的字段（邮编、电话、日期）或带可点击图标的字段（清除、搜索、密码眼睛）。

## 何时不使用
- **多行**文本（备注、评论）→ 使用 [`TextArea`](/docs/ui/widgets/textarea)。
- 从固定选项中选择 → 使用 `Dropdown` / `Option`（见 [`ui/`](/docs/ui)）。

## 外观
带有 `TextFieldStyle` 背景/边框的框，文本在一行内，两端有可选图标，以及可选的一个**浮动 caption**（`floatingCaption`），当有焦点或内容时它会上升到字段上方。

``

## 完整示例（复制、粘贴并运行）
```java
package com.exemplo;

import dev.crossfly.ui.*;
import dev.crossfly.ui.layout.*;

/** 可执行的主界面。将 launcher 指向此类（classname）并运行：gradlew :lwjgl3:run */
public class MainWindow extends Window
{
   @Override
   public void onShow() throws Exception
   {
      super.onShow();
      setLayout(new VerticalLayout());

      // 带浮动 caption 的单行字段
      TextField nome = new TextField().caption("Nome");
      add(LAY_AUTO, nome);

      // 输出 Label
      Label saida = new Label("Digite seu nome e toque em Enviar");
      add(LAY_AUTO, saida);

      // 没有按键 onChange 事件：在提交时读取 getText()
      Button enviar = new Button("Enviar")
         .onAction(c -> saida.setText("Olá, " + nome.getText() + "!"));
      add(LAY_AUTO, enviar);
   }
}
```

> 下面的片段**只展示核心部分**（放入 `onShow()` 内的内容）。要运行它们，请将它们放入上面的 `MainWindow` 骨架中。

## 基本用法
```java
TextField nome = new TextField().caption("Nome");
container.add(LAY_AUTO, nome);
// ...
String valor = nome.getText();
```

## 构造函数
| 构造函数 | 描述 |
|---|---|
| `TextField()` | 空 |
| `TextField(String text)` | 带初始文本（`null` = 空） |

## `EditMode`（编辑模式）
| `EditMode` | 默认键盘 | 备注 |
|---|---|---|
| `NORMAL` | `DEFAULT` | 普通文本；可带自定义掩码 |
| `DATE` | `DATE` | 打开 `DatePicker`；日期掩码 |
| `CURRENCY` | `CALCULATOR` | 货币/小数，掩码从右到左 |
| `NUMERIC` | `NUMERIC` | 数字（桌面端回退到 `KEYPAD`） |
| `TIME` | `TIME` | 打开 `TimePicker` |
| `PASSWORD` | `DEFAULT` | 用 `passwordChar` 代替字符显示 |

`setMode(mode)` 或 `setMode(mode, maskedEdit)` 设置模式；`maskedEdit=true` 启用按掩码的自动格式化。

## 属性与自定义
| 属性 / 方法 | 类型 | 默认值 | 作用 |
|---|---|---|---|
| `text(String)` / `setText(String)` | `TextField` / `void` | — | 设置文本（规范化掩码/货币；光标置于末尾） |
| `getText()` | `String` | — | 显示的文本（**含**掩码，若启用） |
| `getTextWithoutMask()` | `String` | — | "干净"值（仅数字 / 格式化小数） |
| `caption(String)` | `TextField`（流式） | `null` | 浮动 caption / 占位符 |
| `setMode(EditMode[, boolean])` | `void` | `NORMAL` | 编辑模式（+ 掩码） |
| `keyboard(Keyboard)` / `setKeyboard(Keyboard)` | `TextField` / `void` | 按 `EditMode` | 键盘类型 |
| `leftIcon(Icon[, Action])` / `rightIcon(Icon[, Action])` | `TextField`（流式） | `null` | 图标（带可选动作） |
| `leftIconAction(Action)` / `rightIconAction(Action)` | `TextField`（流式） | — | 图标动作（单独设置） |
| `isPassword()` | `boolean` | — | 是否处于密码模式？ |
| `clearText()` | `void` | — | 清除文本、掩码和缓存 |
| 样式 `.mask(String)` | `char[]` | — | 掩码（`9`=数字，其余为字面量）：`"99/99/9999"` |
| 样式 `.validChars(String)` | `String` | `null` | 允许的字符 |
| 样式 `.maxLength(int)` | `int` | `0`（无限制） | 字符数限制 |
| 样式 `.passwordToggle(true)` | `boolean` | `false` | 创建显示/隐藏的"眼睛"图标 |
| 样式 `.floatingCaption(true)` | `boolean` | — | 聚焦/填充时 caption 上升 |
| `getDefaultCurrencyMask(int)` *(static)* | `char[]` | — | 货币掩码（`LocaleFormats` 的分隔符） |

## 回调与事件
| 回调 | 何时触发 |
|---|---|
| `onAction(Action)`（继承） | 在 `onTouchUp`，打开键盘之后 |
| `onFocus()` / `onBlur()`（继承） | 获得 / 失去焦点 |
| `leftIconAction` / `rightIconAction` | 触摸（并松开于）对应图标时 |

没有专门的 `onChange` —— 通过动作作出反应或查询 `getText()`/`getTextWithoutMask()`。

## 状态
- **focused：** 样式 `textFieldFocused(...)`，光标闪烁，触摸时打开键盘/picker；若 `floatingCaption` 则 caption 上升。
- **password：** 有了 `passwordToggle`，右侧图标在 `PASSWORD`↔`NORMAL`（显示/隐藏）之间切换，更新 default 和 focused 样式。
- **disabled / read-only：** `setEnabled(false)` 或 `editable=false` 阻止编辑。

### 带显示/隐藏切换的密码字段
```java
TextField senha = new TextField().caption("Senha");
senha.setMode(EditMode.PASSWORD);
senha.style(Styles.theme()
   .field(FieldRole.PASSWORD, State.NORMAL)
   .passwordToggle(true));   // 自动创建 "眼睛" 图标
container.add(LAY_AUTO, senha);
```

### 带掩码 + DatePicker 的日期
```java
TextField data = new TextField().caption("Data");
data.setMode(EditMode.DATE, true);          // maskedEdit = true
data.style(Styles.theme()
   .field(FieldRole.NORMAL, State.NORMAL)
   .mask("99/99/9999"));
// 点击打开 DatePicker；选择后字段被填充。
container.add(LAY_AUTO, data);
```

### 带验证和清除图标的邮编
```java
TextField cep = new TextField().caption("CEP");
cep.style(Styles.theme()
   .field(FieldRole.NORMAL, State.NORMAL)
   .mask("99999-999")
   .validChars("0123456789"));
cep.rightIcon(new MaterialIcon(""), c -> cep.clearText());  // "close"
container.add(LAY_AUTO, cep);

String limpo = cep.getTextWithoutMask();   // 仅 8 位数字
```

## 最佳实践
- 对于掩码，启用 `maskedEdit`（`setMode(mode, true)`）并在样式中定义 `mask` —— 只定义掩码而不启用 `maskedEdit` 不会自动格式化。
- 持久化/验证时使用 `getTextWithoutMask()`（干净的数字），显示时使用 `getText()`。
- 优先使用正确的 `EditMode`/`Keyboard`，以便为每个字段打开理想的键盘。

## 常见错误
- **定义了 `mask` 但忘记 `maskedEdit`：** 文本不会被格式化。使用 `setMode(mode, true)`。
- **读取 `getText()` 期待干净的数字：** 它返回的是**含**分隔符的；使用 `getTextWithoutMask()`。
- **没有密码模式却用 `passwordToggle`：** toggle 默认已隐含密码，但请确认 `EditMode.PASSWORD` / `FieldRole.PASSWORD` 以获得正确的视觉效果。

## 另见
- 领域指南：[`ui/`](/docs/ui) · 食谱：[`cookbook/`](/docs/cookbook)
- 概念（掩码/验证/焦点 —— 深入）：[`concepts/`](/docs/concepts)
- 相关组件：[`TextInput`](/docs/ui/widgets/textinput) · [`TextArea`](/docs/ui/widgets/textarea) · [`Keypad`](/docs/ui/widgets/keypad)
