diff --git a/AGENTS.md b/AGENTS.md index bb5f11b43..da851205a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -115,7 +115,16 @@ Detailed AI collaboration rules live in [docs/engineering/contributing/agent-gui - Treat AOT compatibility as a first-class design constraint for new features and bug fixes. - Verify with tests or publish checks that match the risk of the change. - Follow the mandatory Superpowers workflow before any creation, change, implementation, or bug fix: invoke `superpowers:using-superpowers`, then route through `superpowers:brainstorming` (new features/components), `superpowers:systematic-debugging` (bugs), `superpowers:test-driven-development` (implementation), `superpowers:writing-plans`/`superpowers:executing-plans` (multi-step work), and `superpowers:verification-before-completion` (before claiming done). Never skip a skill that could apply; only an explicit user instruction may narrow it. -- Prefer declaring template/style bindings (RenderTransform, Transitions, template-part properties, pseudo-class driven setters) in AXAML ControlThemes whenever a selector/Setter can reach the target, the runtime behavior is identical, and there is no measurable performance cost. Fall back to code-behind binding only when the ControlTheme cannot express it — for example targets inside a Content subtree beyond the `/template/` chain, internal members invisible to compiled bindings, runtime-computed values, or AOT-unsafe reflection — and state the reason in a code comment. When an existing code-side binding can be migrated to a ControlTheme with identical behavior and performance, migrate it as part of any change that touches the file. Note: `Transitions` must be declared in ControlTheme style setters, never directly inside ControlTemplate content (template build has no clock attached and throws NRE). + +## 主题绑定优先强约束(本项目生效) + +1. **ControlTheme 优先**:Avalonia 控件的模板/样式类绑定(含 RenderTransform、Transitions、模板部件属性、伪类驱动的 Setter 等),只要同时满足以下全部条件,必须优先放在 AXAML ControlTheme 中声明,禁止先写成代码绑定: + - ControlTheme 能表达该绑定(选择器 / Setter / 模板绑定可达目标元素); + - 逻辑效果与代码绑定完全一致(触发时机、状态条件、取值、动画行为); + - 性能无可测量差异。 +2. **代码绑定是兜底**:仅当 ControlTheme 无法完成时(如目标元素在 Content 子树跨不进 `/template/` 链、依赖 internal 成员而 XAML 编译绑定不可见、目标值为运行时计算值、AOT 约束禁止反射绑定等),才允许放到控件代码中绑定,且必须在代码注释中写明 ControlTheme 不可行的具体原因。 +3. **迁移义务**:发现既有的代码绑定实际上可以等价迁移到 ControlTheme 时,在触及该文件的改动中一并迁移,不留“下次再说”。 +4. **声明位置边界**:`Transitions` 必须声明在 ControlTheme 的 style setter 中,禁止直接写在 ControlTemplate 内容里——模板构建期元素尚未挂载 clock,会抛 NullReferenceException。 ## Common Commands diff --git a/docs/controls/desktop/data-entry/numeric-up-down/changelog.md b/docs/controls/desktop/data-entry/numeric-up-down/changelog.md index 42f19d1d7..81c0cd166 100644 --- a/docs/controls/desktop/data-entry/numeric-up-down/changelog.md +++ b/docs/controls/desktop/data-entry/numeric-up-down/changelog.md @@ -13,6 +13,7 @@ - Behavior - Restore the floating-handle hover shift for the suffix group: since the suffix group moved into the spinner content segment (outside the decorated box `ContentRightAddOn` slot), the animated `ButtonSpinnerDecoratedBox.ContentRightShift` no longer reached it through the template. The transform (`translateX`) and its `TransformOperationsTransition` are declared in the ControlTheme behind the `IsMotionEnabled` gate; `NumericUpDown` code only reacts to the decorated box hover state and replaces the `RenderTransform` value, which arms the transition. Re-mirroring the animated property frame by frame was rejected: the per-frame relay fights the render clock and shows up as a one-frame jump ("flicker") when the pointer re-enters quickly. Note: `Transitions` cannot be declared directly inside ControlTemplate content — the template build has no clock attached and throws NRE — so the transition lives in a style setter. - Fix `InnerLeftContent` never rendering: the prefix presenter lives in a `ButtonSpinner.InnerLeftContent` property-element subtree where `TemplatedParent` never propagates, so its `TemplateBinding`s stayed dead. The presenter is now named `PART_InnerLeftContentPresenter` and relays `InnerLeftContent` / `InnerLeftContentTemplate` in code, matching the suffix presenter pattern. + - Consolidate part bindings into the ControlTheme where the template can express them: the clear button's `Icon` / `IsMotionEnabled`, the text box's `IsCustomFontSize`, and the inner-right presenter's `Content` / `ContentTemplate` (already template-bound since the suffix group moved into direct template content) no longer go through code relays. The remaining code relays are exactly the ones the ControlTheme cannot express: the prefix presenter's property-element subtree (dead `TemplatedParent`), and the clear button's visibility from the internal direct property `IsEffectiveShowClearButton`. - Move the `clear` button from the suffix addon row into the input segment (right edge of the text area, before the spinner action buttons) in both mode templates; its semantic route simplifies to `/template/ .semantic-clear`. - Keep the inner-right content (`suffix` group) attached after the clear button inside the input segment in both mode templates, so `clear`, the user suffix content and the spinner action buttons keep consistent spacing; the `suffix` semantic route simplifies to `/template/ .semantic-suffix`. The Spinner mode adds an 8px inset between the suffix group and the action-button divider, whose content frame has no padding of its own. - Relay the owner `BorderBrush` onto the input frame as a local value (mirroring the shared `AbstractTextInput` behavior and antd `styles.root.borderColor`), so application root border customization wins over the frame state machine; clearing the owner value restores the themed border. diff --git a/src/AtomUI.Desktop.Controls/NumericUpDown/NumericUpDown.cs b/src/AtomUI.Desktop.Controls/NumericUpDown/NumericUpDown.cs index 1de4647e3..75d5febf2 100644 --- a/src/AtomUI.Desktop.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/AtomUI.Desktop.Controls/NumericUpDown/NumericUpDown.cs @@ -488,10 +488,9 @@ public partial class NumericUpDown : AvaloniaNumericUpDown, if (e.NameScope.Find("PART_ClearButton") is { } clearButton) { - _templatePartBindings.Add(BindUtils.RelayBind(this, ClearIconProperty, clearButton, - AbstractIconButton.IconProperty)); - _templatePartBindings.Add(BindUtils.RelayBind(this, IsMotionEnabledProperty, clearButton, - AbstractIconButton.IsMotionEnabledProperty)); + // Only the visibility relay stays in code: IsEffectiveShowClearButton is an internal + // direct property, invisible to compiled template bindings. Icon and IsMotionEnabled + // are template-bound in the ControlTheme. _templatePartBindings.Add(BindUtils.RelayBind(this, IsEffectiveShowClearButtonProperty, clearButton, Visual.IsVisibleProperty)); } @@ -506,19 +505,9 @@ public partial class NumericUpDown : AvaloniaNumericUpDown, innerLeftContent, ContentPresenter.ContentTemplateProperty)); } - if (e.NameScope.Find("PART_InnerRightContentPresenter") is { } innerRightContent) - { - _templatePartBindings.Add(BindUtils.RelayBind(this, InnerRightContentProperty, innerRightContent, - ContentPresenter.ContentProperty)); - _templatePartBindings.Add(BindUtils.RelayBind(this, InnerRightContentTemplateProperty, - innerRightContent, ContentPresenter.ContentTemplateProperty)); - } - - if (e.NameScope.Find("PART_TextBox") is { } textBox) - { - _templatePartBindings.Add(BindUtils.RelayBind(this, IsCustomFontSizeProperty, textBox, - TextBox.IsCustomFontSizeProperty)); - } + // InnerRightContent / InnerRightContentTemplate and the text box IsCustomFontSize are + // template-bound in the ControlTheme (the parts sit in direct template content where + // TemplatedParent propagates), so no code relay is needed for them. // The suffix group renders inside the spinner content segment, outside the decorated box's // ContentRightAddOn slot, so the floating-handle shift no longer reaches it through the diff --git a/src/AtomUI.Desktop.Controls/NumericUpDown/Themes/NumericUpDownTheme.axaml b/src/AtomUI.Desktop.Controls/NumericUpDown/Themes/NumericUpDownTheme.axaml index ff8cf6366..15bd3a5a9 100644 --- a/src/AtomUI.Desktop.Controls/NumericUpDown/Themes/NumericUpDownTheme.axaml +++ b/src/AtomUI.Desktop.Controls/NumericUpDown/Themes/NumericUpDownTheme.axaml @@ -49,6 +49,8 @@ Name="PART_ClearButton" Classes.semantic-clear="True" VerticalAlignment="Center" + Icon="{TemplateBinding ClearIcon}" + IsMotionEnabled="{TemplateBinding IsMotionEnabled}" IsPassthroughMouseEvent="False" />