news 2026/9/10 10:19:48

Bevy Feathers NumberInput 数字输入控件实战:scrubbing 拖拽、限制区间与受控数值绑定的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Bevy Feathers NumberInput 数字输入控件实战:scrubbing 拖拽、限制区间与受控数值绑定的完整指南

Bevy Feathers NumberInput 数字输入控件实战:scrubbing 拖拽、限制区间与受控数值绑定的完整指南

【免费下载链接】bevyA refreshingly simple>项目地址: https://gitcode.com/GitHub_Trending/be/bevy

导读

FeathersNumberInput是 Bevy Feathers UI 组件库中的数字输入控件,本文基于其新增的 scrubbing(按住鼠标拖拽改值)能力展开,讲清该控件的支持类型、HardLimit/SoftLimit/NumberInputPrecision/Step等可选配置组件各自的语义,以及“受控组件”模式下与ValueChange事件的双向同步写法。读完本文,你可以直接用场景 DSL 组装一个具备 Blender 式交互、可限制区间、可带单位换算的数字输入框,并理解其底层状态机与拖拽速度启发式算法。


一、这次更新改了什么:向 Blender 的数字输入交互对齐

在本次重构之前,FeathersNumberInput更像一个"能输入数字的文本框"。更新后该控件从 Blender 的数字输入字段(numeric input)中借鉴了最佳交互元素,核心变化是引入了多种编辑模式并存

  • Scrubbing(拖拽改值):按住鼠标左右拖动即可连续修改数值;
  • 直接键盘输入(direct keyboard entry):点击后进入编辑态,直接键入数字;
  • 与此同时保留了对f32f64i32i64四种数值类型的完整支持,由NumberInputValue的枚举变体决定(见 crates/bevy_feathers/src/controls/number_input.rs)。

这意味着 Feathers 的数字输入控件在交互能力上大大贴近 Blender 的数字字段,适用于属性面板、变换工具(位置/旋转/缩放)、参数化编辑器等需要"快速、连续、精确"同时具备的场景。

二、受控组件模型:数值真相保存在应用侧

FeathersNumberInput是标准 Feathers 风格的受控控件(controlled widget)。所谓受控,指控件内部不会自动维护"当前数值",数值的真实来源始终是应用持有的某个数据结构;控件与数据之间通过两条通道保持双向同步(此设计在 number_input.rs 的文档注释中描述得很清楚):

  1. 用户在控件上拖拽或输入 → 控件发出ValueChange<T>事件 → 应用事件处理器把新值写回应用数据;
  2. 应用数据变化(无论来自该事件还是别处)→ 应用向控件实体插入NumberInputValue组件→ 控件监听组件插入、刷新文本框内容与滑块位置。

ValueChange<T>事件定义在 crates/bevy_ui_widgets/src/lib.rs,除value外还携带一个is_final布尔标志:

  • 拖拽过程中,is_finalfalse,表示值在"预览/连续变化"状态;
  • 拖拽结束、回车提交或失焦时,is_finaltrue,表示这是一个最终提交值。

源码注释特别提醒:即使is_final == false,应用也应该回写控件值(插入新的NumberInputValue),否则用户拖拽时看不到数字实时变化(见 number_input.rs)。

事件处理器的最小写法

feathers_number_input示例展示了这种"数值回写自身"的最小处理器(见 examples/ui/widgets/feathers_number_input.rs):

on( |value_change: On<ValueChange<f32>>, mut commands: Commands| { commands .entity(value_change.event_target()) .insert(NumberInputValue::F32(value_change.value)); }, )

从源码看,控件通过On<Insert<NumberInputValue>>观察者number_input_on_insert_value监听组件插入:若新值与当前文本不同,就用queue_edit(TextEdit::SelectAll)+queue_edit(TextEdit::Insert(...))替换文本框内容,同时调用update_slider_pos把背景滑块条移动到对应位置(见 number_input.rs)。为减少无谓刷新,官方建议只在值真正变化时插入NumberInputValue(见 number_input.rs)。

三、行为配置组件:四件套 + 环绕模式

控件行为通过若干可选组件(直接随场景 DSL 挂到@FeathersNumberInput实体上)来配置。它们的语义是本次发布说明的核心内容,下面逐一展开。

HardLimit —— 绝对硬边界

HardLimit指定数值的绝对最小/最大范围(对应NumberInputRange的一个区间)。任何超出该范围的取值都会被clamp(截断)回边界内,无论来自拖拽、步进还是键入。

  • 若该组件缺省,则使用该数据类型自身的自然范围(如f32即整个 32 位浮点范围)。
  • 按类型提供便捷构造:HardLimit::f32(range)HardLimit::f64(range)HardLimit::i32(range)HardLimit::i64(range)(见 number_input.rs)。
  • clamp 行为本身定义在NumberInputRange::clamp(见 number_input.rs)。注意若区间类型与值类型不匹配,会warn_once并原样返回值。

SoftLimit —— 仅约束拖拽的“软边界”

SoftLimit指定仅通过拖拽可到达的范围;通过键入输入的值可以超出这个范围。这正是它叫“软”的原因——它不是上限,而是拖拽舒适区。

  • 例如SoftLimit(NumberInputRange::F32(0.0..=10.0))意味着拖拽最多改到 0~10,但键盘可以输入 25.5。
  • 应用场景:既希望鼠标操作限定在合理区间防止误拖,又保留高级用户直接输入越界值的灵活性。

NumberInputPrecision —— 拖拽精度量化

NumberInputPrecision(pub i32)表示拖拽时保留的小数位数

  • 值为2→ 四舍五入到最近的 1/100;
  • 值为0→ 取整;
  • 值为负数(如-3)→ 四舍五入到最近的千位(见 number_input.rs)。

它的作用仅限于拖拽过程中量化数值,避免小数点后面一串数字乱跳;不影响键盘键入,也不阻止应用通过其他途径(如直接插入NumberInputValue)设置非量化值(见 number_input.rs 注释)。实现上NumberInputPrecision::roundF32/F64计算10^n因子做乘后取整再除;对整数类型保持恒等映射(见 number_input.rs)。

NumberInputStep —— 步进增量

NumberInputStep(pub f64)表示递增/递减时单次改变的增量(默认1.0)。当光标悬停在无软边界的输入框上时会出现左右两个 chevron 箭头(NumberInputDecrement/NumberInputIncrement),点击即按该步长加减;拖拽速度计算也会参考该值(见下文)。构造与默认值见 number_input.rs。

发布说明中描述的是Step,实际实现中的组件名为NumberInputStep,二者指向同一配置语义。

NumberInputWrap —— 硬边界环绕

除四件套外,实现中还提供了NumberInputWrapNoWrap/Wrap)组件:当存在HardLimit且开启Wrap时,越界值会wrap(环绕)回区间内而非被 clamp,典型用例是角度/朝向这类周期性数值。环绕实现在NumberInputRange::wrap(基于rem_euclid,见 number_input.rs);若配置了Wrap却没有HardLimit,会告警并忽略环绕(见 number_input.rs)。

各约束的合成顺序

当一个拖拽/提交同时携带多个约束时,应用顺序是有讲究的。emit_drag_value_change中可以看到完整流水线(见 number_input.rs):

  1. 以拖拽起始值base_value为基准,累加偏移得到候选值(每次拖拽是相对拖拽开始值的增量,而不是相对上一帧值);
  2. 若有SoftLimit→ clamp 进软范围;
  3. 若有NumberInputPrecision→ 按小数位量化;
  4. 最后处理硬边界:HardLimit+Wrap则环绕,否则 clamp。

硬边界"最后应用",确保任何路径产出的值都不可能越出硬边界。

四、两种交互外观:Slider 模式与 Scrubber 模式

控件的外观与拖拽手感取决于是否配置SoftLimit(这是本次发布说明区分出的两种模式)。

有 SoftLimit:看起来像一个滑块

SoftLimit存在时,控件观感与手感都更接近 FeathersSlider

  • 输入框背景会绘制一条滑轨(slide bar),数值所在位置有一条与滑块 thumb 等宽的“已填充”条;
  • 拖拽速度按公式(范围长度 / 滑轨像素宽度)计算(见 number_input.rs),也就是鼠标移动多少像素,值就按比例覆盖多少范围,鼠标位移与滑轨尺寸严格同步——这正是发布说明中“changes in the bar's size will be synchronized with movement of the mouse”的含义;
  • 滑轨位置由update_slider_pos依据NumberInputRange::thumb_position计算出的 0~1 比例实时更新到BackgroundGradient的渐变色标上(见 number_input.rs),这种"用渐变画滑块"的做法可以让滑轨也保持圆角。

无 SoftLimit:更像一个“擦子”(Scrubber)

SoftLimit不存在时,控件不画滑轨,表现为典型的“scrubber”:

  • 拖拽速度不再由滑轨决定,而是依据一套启发式算法综合当前可用信息推断(见 number_input.rs),优先级从高到低为:
    1. NumberInputStep→ 速度 =step × BASE_DRAG_SPEEDBASE_DRAG_SPEED = 0.01,见 number_input.rs);
    2. 是整数类型(I32/I64)→ 视为 step=1,使用基础速度;
    3. NumberInputPrecision→ 由精度反推速度 =10^(-precision)
    4. 以上都没有 → 使用自适应算法:取当前值绝对值的数量级(距 1 最近的 10 的幂)作为量级缩放,BASE_DRAG_SPEED × 10^decade,保证大数拖得快、小数拖得慢。

增强细节:Shift 慢速微调

scrubber_on_drag中还有一个实用设计:拖拽时按住Shift,位移增量乘以0.1,实现慢速精细调节(见 number_input.rs)。

五、点击 vs 拖拽:内部状态机如何区分两种手势

两种模式共同的前提是:需要区分"用户是想拖拽改值"还是"想点一下进入输入模式"。控件用一个DragState组件 +EditMode枚举(Idle/Scrubbing/Editing,见 number_input.rs)管理状态迁移,并定义一个DRAG_THRESHOLD_DISTANCE = 0.5像素阈值(见 number_input.rs)。

从事件处理器可以梳理出完整的判定流程:

  1. 输入框内部有一个绝对定位、覆盖整个输入区、用于拦截指针事件的透明子实体(scrubber),承载 press/release/drag_start/drag/drag_end/drag_cancel 一系列观察者(见 number_input.rs);
  2. scrubber_on_press:非编辑态下按下 → 进入Scrubbing模式并把TextReadWriteMode切为Static(隐藏光标),同时propagate(false)阻止事件继续传给底层文本编辑(见 number_input.rs);
  3. scrubber_on_drag:实时记录累计最大位移max_distance,只有当位移超过 0.5px 阈值才开始改值(防止点击时的抖动误触发)——这是“click vs drag”判定的核心(见 number_input.rs);
  4. scrubber_on_release:若整次按下释放过程中max_distance从未超过阈值 → 视为一次点击,切换进Editing模式:恢复TextReadWriteMode::Editable、光标变成 I-Beam,并根据点击位置把文本光标MoveToPoint定位过去(见 number_input.rs);
  5. 输入过程中若发生PointerCancel(如窗口失焦打断),scrubber_on_drag_cancel会把状态还原为Idle(见 number_input.rs)。

拖拽/悬停时控件的滑轨与配色由set_slidebar_styles统一刷新:它根据 disabled / pressed / hovered / focused 状态在SLIDER_BAR_*SLIDER_BG_*TEXT_INPUT_TEXT_*等主题 token 之间切换颜色,并同步设置系统光标形状(禁用时NotAllowed,否则ColResize,见 number_input.rs)。

点击进入编辑模式后的行为

非拖拽点击会激活typing 模式,用户可以输入数字直接改值:

  • 回车(Enter)提交:number_input_on_enter_key把当前文本按控件声明的格式解析,应用硬边界与环绕约束后触发一次is_final = trueValueChange,并把模式切回Idle(见 number_input.rs);
  • 失焦提交:number_input_on_focus_lost走同样的解析提交路径并恢复光标与只读模式(见 number_input.rs);
  • 解析失败时只warn!("number input parsing failed, invalid format")并放弃该次提交(见 number_input.rs);
  • 若文本被清空为空串,则不触发事件(见 number_input.rs)。

六、类型支持与格式解析:NumberInputValue 是如何分派的

NumberInputValue是承载数值的组件,也是一个带类型的枚举:

pub enum NumberInputValue { F32(f32), F64(f64), I32(i32), I64(i64), }

默认值为F32(0.0)(见 number_input.rs)。由于FeathersNumberInput通过#[require(NumberInputValue)]要求该组件,创建控件时只需直接插入一个带值的NumberInputValue变体即可设定初始值并决定控件的数据格式(见 number_input.rs)。

NumberFormat枚举(默认F32)记录当前编辑格式,它决定发出的ValueChange<T>泛型类型:trigger_value_change会按枚举变体把commands.trigger(ValueChange { source, value, is_final })分派为ValueChange<f32>/ValueChange<f64>/ValueChange<i32>/ValueChange<i64>四者之一(见 number_input.rs)。事件中携带的source实体即FeathersNumberInput根实体,应用可以据此区分是哪个输入框发出的变化。

整数变体的加减采用saturating_add防止溢出(见 number_input.rs)。

七、带单位的数字输入:Units 注册表与展示换算

除了裸数值,实现还提供了成熟的单位系统,让同一个控件既显示"45°"又能以"45d"编辑:

  • NumberInputUnits:挂在控件上的引用组件,内容是一个字符串 id(如"length_meters""angle_degrees"),指向注册表中某个UnitsFormat(见 number_input.rs);
  • UnitsFormattrait:定义id()format(value, editing)(按展示/编辑两种模式格式化字符串)、parse(...)(把带后缀字符串解析回规范单位数值),见 number_input.rs;
  • StandardUnitKind:以"后缀表 + 换算比例表"的形式便捷实现UnitsFormat,并支持canonical_index(内部存储单位,如弧度)/display_index(展示单位,如度)/editing_index(编辑单位,如 "d")三套索引分离,见 number_input.rs。

内置了三组标准单位(见 number_input.rs):

类型id典型后缀内部规范单位
Dimensionless"none"
LengthMeters"length_meters"m / km / cm / mm / ft / in / mi
TimeSeconds"time_seconds"s / ks / cs / ms
AngleDegrees"angle_degrees"rad / ° / d / deg弧度(显示为度)

有意思的细节:角度单位内部规范量是弧度,但展示时输出45°;由于°不方便键入,编辑模式会自动把内容替换为45ddisplay_indexediting_index分离正是为此,见 number_input.rs)。scrubber_on_release在进入编辑态前也会先把显示文本替换为"可编辑格式",并在结束时换回展示格式(见 number_input.rs)。

这些单位对象由UnitsRegistry资源集中管理,NumberInputPluginPlugin::build时默认注册了上述四者,并挂接两个系统在PickingSystems::LastInputFocusSystems::Dispatch之后同步滑轨配色(见 number_input.rs)。注册表支持通过insert注入自定义单位(见 number_input.rs)。解析/格式化的正确性有单元测试兜底,例如test_length_meters_convert_km_to_m("1km" → 1000m)、test_angle_degrees_format(π/4 rad → "45°" / 编辑时 "45d")、test_parse_negative_numbers("-50.5cm" → -0.505m)等(见 number_input.rs)。

八、从零搭一个输入框:完整示例

控件本身是一个可继承的 Scene 组件(#[derive(SceneComponent)],见 number_input.rs),在 bsn 场景 DSL 中用@FeathersNumberInput生成。仓库中的feathers_number_input示例(examples/ui/widgets/feathers_number_input.rs)把各种配置组合平铺展示,节选如下:

fn demo_root() -> impl Scene { bsn! { ... Children[ @demo_field_f32("soft limit", 2.0, bsn!( SoftLimit(NumberInputRange::F32(0.0..=10.0)) )), @demo_field_f32("hard limit", 3.0, bsn!( HardLimit(NumberInputRange::F32(-100.0..=100.0)) )), @demo_field_f32("soft + hard", 4.0, bsn!( SoftLimit(NumberInputRange::F32(0.0..=10.0)) HardLimit(NumberInputRange::F32(-100.0..=100.0)) )), @demo_field_f32("precision(2)", 6.0, bsn!( NumberInputPrecision(2) )), @demo_field_f32("hard limit + wrap", 0.0, bsn!( HardLimit(NumberInputRange::F32(-180.0..=180.0)) NumberInputWrap::Wrap )), @demo_field_f32("in meters", 2.0, bsn!( NumberInputUnits::new(&LengthMeters) )), @demo_field_f32("in degrees", PI, bsn!( NumberInputUnits::new(&AngleDegrees) )), ] } } fn demo_field_f32(label_text: &str, value: f32, options: impl Scene) -> impl Scene { bsn! { ... Children [ ( @FeathersNumberInput NumberInputValue::F32(value) @{options} Node { flex_grow: 1.0, max_width: px(120) } on( |value_change: On<ValueChange<f32>>, mut commands: Commands| { commands.entity(value_change.event_target()) .insert(NumberInputValue::F32(value_change.value)); }) ), ] } }

要点拆解:

  • @FeathersNumberInput生成控件骨架,其内部结构(标签段、可编辑文本、透明 scrubber 层、左右 chevron)由FeathersNumberInput::scene预置(见 number_input.rs);
  • NumberInputValue::F32(value)直接设置初始值与数值格式;
  • 可选行为组件(SoftLimitHardLimitNumberInputPrecisionNumberInputStepNumberInputWrapNumberInputUnitsInteractionDisabled)用@{options}展开注入;
  • 每个输入框必须配套On<ValueChange<f32>>(或对应泛型)处理器做受控回写;
  • InteractionDisabled组件可让控件进入禁用态:文本只读、光标变为NotAllowed、滑轨与文字改用 disabled token(相关逻辑见 number_input.rs)。

需要 f32 / i32 标准字段时,也可以直接复用仓库封装好的 helper(注意它们需要bevy_feathersfeature 开启):examples/helpers/number_input_f32.rs 提供number_input_f32(name, identifier, value, precision, limits),内部组合了HardLimit::f32(limits)与可选标识组件;examples/helpers/number_input_i32.rs 提供number_input_i32(...)。标识组件用来在多输入框场景中区分事件来源——查询哪个带number_input_identifierFeathersNumberInputValueChangesource实体即可。

视觉定制:sigil 与 label

创建控件时还可通过FeathersNumberInputProps定制两处外观(见 number_input.rs):

  • sigil_color:输入框左侧的一条彩色竖条(默认透明),常用于区分不同坐标轴(如 X/Y/Z 分别用不同 token 着色,示例中用的是tokens::TEXT_INPUT_X_AXIS);
  • label_text:sigil 右侧的静态说明文字,惯例填 "X" / "Y" / "Z",带 label 时 sigil 会加宽为 4px 的左边框。

在 bsn 中使用 props 的写法是属性语法:@FeathersNumberInput { @sigil_color: ..., @label_text: "X" }(见 examples/ui/widgets/feathers_number_input.rs)。

运行示例

在启用bevy_feathersfeature 的前提下,可用 cargo 直接运行该示例:

cargo run --example feathers_number_input --features bevy/bevy_feathers

(具体 feature 名称以仓库 Cargo.toml 中的配置为准。)

九、升级迁移:用插入组件取代触发事件

如果你是从旧 API 迁移到本次重构版本,需要特别注意程序化更新数值的通道变了:旧版本通过触发UpdateNumberInput事件来更新值,新版本改为插入NumberInputValue组件(详见 迁移指南)。后者在创建时指定初始值也更容易:

// BEFORE(旧 API,已废弃) commands.trigger(UpdateNumberInput { entity: input_ent, value: NumberInputValue::F32(new_value), }); // AFTER(新 API) commands .entity(input_ent) .insert(NumberInputValue::F32(new_value));

其余不变:事件处理器仍然监听ValueChange<T>ValueChangeis_final = false(拖拽中)与= true(提交)的语义配合受控回写,可以实现"拖拽实时预览、松开才落库"的编辑器级体验。

结语

把本次发布说明与 number_input.rs 的实现对照来看,FeathersNumberInput的交互模型可以概括为三句话:可选组件决定能力(限制/精度/步进/单位/环绕),有无SoftLimit决定观感(滑块还是擦子),受控回写决定数据流(事件外发 + 组件插入回流)。借助0.5px的手势阈值与EditMode状态机,点击与拖拽被可靠区分;借助启发式拖拽速度与 Shift 微调,从千分位小值到十万级大值都能用一只手顺滑调整。这套能力对需要高密度数值编辑的工具型 UI 非常实用,值得在属性面板、变换 gizmo 或参数化设置界面中优先采用。

【免费下载链接】bevyA refreshingly simple>项目地址: https://gitcode.com/GitHub_Trending/be/bevy

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 10:19:31

OpenClaw 2.0 Active Memory:用 SQLite + MCP 实现办公 Agent 真实记忆

1. 项目概述&#xff1a;OpenClaw 2.0 不是又一个“玩具Agent”&#xff0c;它在解决办公场景里最真实的记忆断层“OpenClaw 2.0 补上了 Agent 的记忆&#xff0c;办公场景还差最后一公里”——这句话不是营销话术&#xff0c;而是我连续三周泡在真实办公流里反复验证后写下的结…

作者头像 李华
网站建设 2026/9/10 10:18:38

CANN/GE图引擎ResolveBuilder接口

ResolveBuilder 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、TensorFlow…

作者头像 李华
网站建设 2026/9/10 10:17:55

CANN/ge SetSubgraph API文档

SetSubgraph 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、TensorFlow 前…

作者头像 李华
网站建设 2026/9/10 10:16:58

MATLAB云模型实现:正向与逆向云发生器原理及综合评价实战

做数据分析或者决策评价的朋友&#xff0c;应该都遇到过这种问题&#xff1a;专家打分给的是“大概85分”、“质量很好”、“风险较高”这类带模糊性的描述&#xff0c;直接拿一个具体数字去算&#xff0c;总感觉丢了信息&#xff1b;完全用模糊数学的隶属度来描述&#xff0c;…

作者头像 李华
网站建设 2026/9/10 10:15:00

拖曳伞空中回收的缆绳系统动力学建模与高斯最小约束原理应用

简介&#xff1a;针对微型空中飞行器&#xff08;MAV&#xff09;在空中回收过程中面临的缆绳-拖曳伞系统动态建模难题&#xff0c;这套Matlab代码基于高斯原理推导拖缆系统的运动方程&#xff0c;并构建了完整的参数化仿真环境。资源面向计算机、电子信息工程、数学等专业的大…

作者头像 李华