Files

17 KiB
Raw Permalink Blame History

LineEdit 桌面版实现原理

本文档描述 LineEdit 桌面版输入家族的内部状态同步、模板接入、清除/reveal/字数统计、SearchEdit、TextArea resize、Form 和 CompactSpace 集成。共享输入分层见 输入控件共享架构设计,公共设计与 API 契约见 LineEdit 桌面版架构设计,Token 语义见 LineEdit Token 设计,变化记录见 LineEdit Changelog。

1. 实现定位

LineEdit 家族的实现以 Avalonia TextBox 为文本编辑内核,AtomUI 分为 AbstractTextInput 逻辑层和 InputControlFrame 视觉层:前者负责文本输入扩展、状态归一和生命周期,后者负责输入表面和有效状态渲染。实现文档聚焦 AtomUI 增强层,不重新说明 Avalonia 文本编辑、选择和滚动算法。

2. 源码文件结构

主要源码:

  • src/AtomUI.Desktop.Controls/Input/AbstractTextInput.cs:文本输入逻辑基类,统一 StyleVariant、Status、SizeType、清除、reveal、字数统计、Form、native validation、CompactSpace 和 viewport metrics。
  • src/AtomUI.Desktop.Controls/Primitives/InputControlFrame.cs:internal 输入表面组合控件,统一边框、背景、圆角、shadow、交互状态、有效状态和 CompactSpace 视觉。
  • src/AtomUI.Desktop.Controls/Input/TextBox.cs:AtomUI 基础 TextBox,继承 AbstractTextInput,提供基础文本编辑模板。
  • src/AtomUI.Desktop.Controls/Input/LineEdit.cs:标准单行输入框,继承 AbstractTextInput,提供外部 AddOn、内部右侧内容绑定和单行布局。
  • src/AtomUI.Desktop.Controls/Input/SearchEdit.cs:搜索输入框,提供搜索按钮样式、搜索按钮文本、加载态和搜索点击事件。
  • src/AtomUI.Desktop.Controls/Input/TextArea.cs:多行输入框,继承 AbstractTextInput,提供固定行数、自动高度和 resize。
  • src/AtomUI.Desktop.Controls/Input/InputTextPresenter.cs:输入文本 presenter,处理 Avalonia 12 selection foreground 缓存刷新。
  • src/AtomUI.Desktop.Controls/Input/SearchEditDecoratedBox.cs:SearchEdit 输入壳体与搜索按钮协作。
  • src/AtomUI.Desktop.Controls/Input/TextAreaDecoratedBox.cs:TextArea 输入壳体、scroll viewer 和 resize 相关协作。
  • src/AtomUI.Desktop.Controls/Input/TextViewportMetrics.cs:输入控件向同程序集消费方发布有效文本 viewport 宽度的内部度量契约。
  • src/AtomUI.Desktop.Controls/Input/ResizeHandle.cs:TextArea resize 拖拽入口。
  • src/AtomUI.Desktop.Controls/Input/TextBoxToken.cs:TextBox 专属内容 padding Token。
  • src/AtomUI.Desktop.Controls/Input/LineEditToken.cs:单行输入字号 Token。
  • src/AtomUI.Desktop.Controls/Input/TextAreaToken.cs:TextArea 字号、右侧 padding 和 resize Token。
  • src/AtomUI.Desktop.Controls/Input/Themes/*.axaml:TextBox、LineEdit、SearchEdit、TextArea 和内部按钮主题。

3. 核心类职责

AbstractTextInput 是文本输入逻辑唯一 owner。它统一维护 StyleVariant、Status、SizeType、IsMotionEnabled、清除/reveal/count、placeholder、Form value、native validation 接入、FormStatus、CompactSpace 和 TextViewportMetrics。TextBox、LineEdit、TextArea 只补充单行、多行或 AddOn 专用结构,不重复声明这些共有状态。

InputControlFrame 是输入表面视觉唯一 owner。它从 AbstractTextInput 接收已经归一化的 StyleVariant、SizeType、IsMotionEnabled、Status、FormStatus、DataValidationErrors、IsEnabled、focus/hover/pressed 和 CompactSpace 输入,计算 EffectiveStatus,并渲染边框、背景、圆角、shadow、error/warning、disabled 与 motion。它不拥有文本值,也不负责 AddOn 内容布局;DataValidationErrors 仍是 frame 上的 native validation 唯一真源,不在 frame 外形成第二套验证源。

AddOnDecoratedBox 继承 InputControlFrame,只增加 AddOn、内部内容和布局扩展;SearchEditDecoratedBox、TextAreaDecoratedBox 以及 Select/TreeSelect/Cascader/ButtonSpinner 的专用 decorated box 继续复用该 frame 状态模型,不得重新计算输入表面状态。

SearchEdit 在 LineEdit 基础上把搜索按钮加入输入壳体。搜索按钮和 Enter 键统一调用 RaiseSearchRequested(),再由控件抛出包含查询文本快照和触发来源的 SearchRequested 路由事件。

TextArea 继承 AbstractTextInput,因为多行输入需要不同模板、scroll viewer 接入、固定行数测量和 resize 流程。它直接复用基类状态、尺寸、清除、字数统计和 Form 模型。

TextViewportMetrics 是不对用户公开的响应式度量通道。TextBox/TextArea 仍是自身模板结构和文本可视区域的唯一 owner;消费方只能读取发布后的有效宽度,不能获得 TextPresenter、ScrollViewer 或 NameScope,也不能反向遍历输入控件的 VisualTree。

4. 状态与数据流

文本值流:

User input / Form.SetValue
      ↓
Text
      ↓
TextChanged class handler
      ↓
CountText + Form ValueChanged

清除按钮流:

IsAllowClear / IsReadOnly / Text / AcceptsReturn
      ↓
IsEffectiveShowClearButton
      ↓
PART_ClearButton.IsVisible
      ↓
Click → Clear()

Form 与有效状态流:

Form.SetValue(object?) → Text
TextChanged            → IFormItemAware.ValueChanged
Form.GetValue()        → Text
Form.ClearValue()      → Text = null
DataValidationErrors   → InputControlFrame
Form error             → Form-owned DataValidationErrors entry
Form warning/status    → FormStatus → InputControlFrame
Status                 → InputControlFrame
EffectiveStatus        → Native Error / Form Error > Form Warning > Explicit Warning > Explicit Error > Default
Form feedback control  → FormFeedback + IsFormFeedbackVisible

文本 viewport 度量流:

TextBox/TextArea own template parts
      ↓
ScrollViewer.Viewport / Padding + TextPresenter.Margin
      ↓
TextViewportMetrics.ViewportWidth
      ↓
OverflowTip and other internal consumers

状态伪类:

  • DataValidationErrors 是 native error 的唯一真源,并通过模板绑定投射到 frame。
  • Form error 由 Form 写入自己的 native validation entry,Form reset 只撤销该 entry。
  • Form warning、success、validating 进入 FormStatus,不修改用户显式 Status;最终是否遮蔽显式状态由 EffectiveStatus 优先级决定。
  • Status=Error/Warning 只形成 ExplicitStatus,不写入 native error。
  • InputControlFrame 根据 EffectiveStatus 设置 :error、:warning 及对应主题状态;:warning 仅在最终有效状态为 Warning 时存在。
  • StyleVariant=Outlined/Filled/Borderless/Underlined 由 frame 统一选择器表达。

5. 生命周期与模板接入

AbstractTextInput.OnInitialized 在未设置 ClearIcon 时写入默认 CloseCircleFilled,并建立 Form/native validation 与 viewport 度量边界。控件 token scope 和主题 asset 由 generator 显式注册;TextBox、LineEdit、TextArea 的专属 Token 只表达各自文本布局或尺寸差异。

OnApplyTemplate 规则:

  • 旧清除按钮 click 订阅必须解除,再绑定新 PART_ClearButton。
  • AbstractTextInput 每次套用模板都重新获取 PART_InputControlFrame,并把共有状态回放到 frame;frame 负责初始 transitions 抑制、边框厚度和 CompactSpace 状态计算。
  • TextBox 的 PART_InputControlFrame 必须水平填满 TextBox 已分配宽度;placeholder、短文本和长文本只改变内部文本布局,不改变输入外框宽度。
  • AddOnDecoratedBox 派生 part 只接收 frame 状态并建立 AddOn/内部内容布局,不再承担独立状态选择器。
  • clear/reveal/form/inner-right/count 等稳定 control-to-template 状态由 AXAML TemplateBinding 或显式 typed ancestor binding 表达;AddOnDecoratedBox.ContentRightAddOn 跨模板边界内的节点使用 $parent[atom:<InputType>] 绑定到对应输入控件。
  • SearchEdit 获取 SearchEditDecoratedBox 后设置 OwningSearchEdit,由 decorated box 回调搜索事件。
  • TextArea 获取 TextAreaDecoratedBox 后设置 Owner,获取 ResizeHandle 后设置 Owner。
  • TextBox / TextArea 每次套用模板时先释放旧文本 viewport source 订阅,再由当前模板中的 scroll viewer 和 text presenter 发布新的有效宽度;旧 part 后续变化不得再影响控件度量。
  • TextAreaDecoratedBox 只在自己的 OnApplyTemplate 中获取 PART_ScrollViewer,再通过直接 owner 协作把 source 交给 TextArea。TextArea 和外部 behavior 都不得进入 decorated box 的模板查找该 part。
  • 模板 part 的 click、preedit 和 viewport 订阅随当前模板实例存续,只在重新套用模板前释放;logical detach 不销毁仍然有效的模板连接。
  • Form feedback 属于外部对象订阅:在 FormFeedback 变化时替换,在 logical detach 时释放,并在 logical attach 时按当前 feedback 状态重新建立。

模板 part 属于主题契约。需要调整内部视觉时,应优先在 AXAML 中维护静态模板和 selector,不把 clear/reveal/search/resize 视觉搬到 C# 动态创建。

6. 交互与事件处理

清除按钮点击进入 NotifyClearButtonClicked(),默认调用 Clear()。派生控件可以重写 hook,但必须保持 Text、Form value 和字数统计同步。

密码 reveal 通过 RevealButton.IsChecked 与 RevealPassword 双向绑定完成。IsEnableRevealButton 只控制按钮可见性,不改变 PasswordChar 自身语义。

SearchEdit 搜索请求流程:

Search button click
  → SearchEditDecoratedBox
  → SearchEdit.RaiseSearchRequested(Button)
  → if !IsOperating raise SearchRequested

Enter KeyUp when enabled and unhandled
  → mark handled
  → SearchEdit.RaiseSearchRequested(EnterKey)
  → if !IsOperating raise SearchRequested

TextArea resize 流程:

Pointer pressed → NotifyAboutToResize()
Pointer moved   → NotifyResizing(delta)
Pointer released→ NotifyResizeCompleted()

NotifyAboutToResize() 记录原始高度,并根据 MinLines / MaxLines 和当前 scroll viewer 垂直开销计算可拖拽高度范围。

7. 内部算法与关键流程

7.1 字数统计

IsShowCount=true 时,输入变化更新 CountText,格式为 当前长度 / MaxLength。MaxLength 来自 Avalonia TextBox,当业务需要无限长度时仍应评估显示文案是否合理。

7.2 CompactSpace 边框厚度

InputControlFrame 根据 CompactSpaceOrientation 和最终 StyleVariant 计算当前边框厚度、圆角折叠和相邻控件连接关系。AbstractTextInput 只发布 CompactSpace 状态,不直接读取具体边框节点;非 outlined 表面由 frame 返回无边框折叠值。

7.3 TextArea 固定行数测量

TextArea 在 IsAutoSize=false、Lines>0 且未显式设置 Height 时,使用 TextLayout 计算指定行数的文本高度,再叠加 scroll viewer 与 text presenter 的垂直空间,写入 scroll viewer 的 MinHeight / MaxHeight。

7.4 TextArea resize 边界

TextArea resize 以控件当前 Bounds.Height 为起点。拖拽时高度被限制在 MinLines 和 MaxLines 对应的 scroll viewer 高度范围内;若 MaxLines<=0,最大值为正无穷。

7.5 Selection foreground 刷新

InputTextPresenter 在 SelectionStart、SelectionEnd、SelectionForegroundBrush 和 ShowSelectionHighlight 变化时调用 InvalidateTextLayout(),用于避免 Avalonia 12 文本 run 缓存导致选中文本前景色滞留。

7.6 文本 viewport 宽度

文本是否在输入表面内可见,必须以 scroll viewer 的 viewport 为起点,不能使用会随文本内容扩展的 TextPresenter.Bounds.Width,也不能由消费方使用 GetVisualDescendants() 查找 PART_TextPresenter。

当前模板的有效宽度按以下语义计算:

effective width = ScrollViewer.Viewport.Width
                - ScrollViewer horizontal padding
                - TextPresenter horizontal margin

外壳 padding、border、左右 AddOn、clear/reveal/feedback/count 等布局占用应由 scroll viewer 的实际 viewport 或 padding 反映,不能在 OverflowTip 中复制控件模板补偿。内部度量使用三态:null 表示当前模板没有精确 source、NaN 表示 source 已接入但布局尚未产生有效宽度、正数表示可消费的有效宽度。

8. 资源、性能与 AOT 边界

LineEdit 家族不依赖运行时反射发现模板结构。固定 control-to-template 状态通过稳定 part、TemplateBinding 和 typed ancestor binding 表达;运行时协作通过接口和显式 owner 引用完成。

资源和生命周期边界:

  • 固定模板状态不创建 C# relay binding;ContentRightAddOn 内容边界内使用 typed ancestor binding,生命周期由模板拥有。
  • _feedbackStatusSubscription 必须在 FormFeedback 变化和 logical detach 时释放,并在 logical attach 时重新建立。
  • clear button click 订阅必须在新模板接入前解绑旧按钮。
  • preedit 和文本 viewport source 订阅必须在新模板接入前释放旧 source;普通 logical detach 不释放当前模板仍需使用的订阅。
  • 文本 viewport source 的 Viewport、Padding 和 presenter Margin 订阅必须由 TextBox/TextArea 持有,并在模板重套用时成组替换。
  • TextArea resize 不创建全局订阅;拖拽状态保存在控件实例字段中。
  • SharedToken 与 frame 主题表达输入表面值;TextBox/LineEdit/TextArea Token 只表达稳定的字体、padding 和 resize 视觉语义,不承载文本值、清除状态、Form 状态或搜索运行状态。

AOT 边界:

  • Token 类型通过 generator 显式注册。
  • API 与 Token 契约由控件文档、源码 public surface、Token 类型或生成数据维护,不依赖运行时反射扫描。
  • 文档中描述的 template part 名称应与 AXAML 和 C# 查找代码保持一致。

9. 维护不变量

内部重构必须保持以下不变量:

  • TextChanged 继续驱动字数统计和 Form value changed。
  • 清除按钮可见性不在 AXAML 与 C# 中形成相互冲突的状态源。
  • TextBox 外框宽度由父布局和 Width / MinWidth / MaxWidth 契约决定,不能随 placeholder 或当前文本的测量宽度伸缩。
  • IsCustomFontSize=true 不能被 SizeType 字体样式覆盖。
  • InputControlFrame.EffectiveStatus 必须遵守 Native Error / Form Error > Form Warning > Explicit Warning > Explicit Error > Default;native error 由 DataValidationErrors 唯一提供,Form reset 不得清理外部 native error。
  • 所有 *AddOnDecoratedBox 只能扩展 frame 布局,不能重新定义 variant/status/error/warning/disabled/motion selector。
  • SearchEdit.IsOperating=true 必须阻止按钮和 Enter 键产生重复搜索请求。
  • TextArea 的 fixed lines、auto-size 和 resize 不互相覆盖高度状态。
  • 重新套用模板不能泄漏旧按钮 click、旧模板 binding、旧 preedit 或旧 viewport source;logical reattach 后当前模板交互与状态绑定必须保持有效,Form feedback 必须重新订阅。
  • TextPresenter margin 是输入模板视觉契约;文本有效宽度由输入控件在模板所有权边界内统一计算并发布,不在业务控件或消费 behavior 中加入隐藏补偿。
  • 输入控件不得向内部消费方暴露 TextPresenter / ScrollViewer 实例;模板结构变化只能影响输入控件自己的度量实现。

10. 测试与验证

验证范围:

  • LineEditShowCasePageTests 覆盖 Gallery 页面结构、示例 snapshot 和源码片段入口。
  • 清除按钮:空文本、非空文本、read-only、TextArea 和 single-line 差异。
  • TextBox 布局:显式宽度和父布局分配宽度保持稳定,placeholder、单字符和长文本切换不改变 input frame 宽度。
  • 生命周期:基础输入、全部内部派生输入和 AutoComplete 组合宿主在 logical detach/reattach 后保持 reveal、clear、inner-right、IME preedit、viewport 和 Form feedback 连接。
  • SizeType:Large/Middle/Small/Custom 字号、高度、line height 和 IsCustomFontSize 优先级。
  • Variant/status:Outlined、Filled、Borderless、Underlined、Error、Warning、focus、disabled。
  • AddOn:外部 left/right AddOn、内部 left/right content、Form feedback、字数统计顺序。
  • SearchEdit:Default/Primary 搜索按钮、loading、搜索事件和 disabled。
  • TextArea:Lines、MinLines、MaxLines、IsAutoSize、IsResizable、resize 边界和字数统计。
  • 文本 viewport:clear/feedback/addon 改变内部可用宽度但 owner Bounds 不变时发布新值;模板重套用后旧 source 不再发布;TextArea scroller padding 和 presenter margin 被正确扣除。
  • 文档改动运行 git diff --check,并检查相对链接存在。