标注属性对话框
当用户双击图表上的标注时,图表内部会自动识别双击手势并推送 ChartEvent::DoubleClick(ChartElement::Annotation(id)) 事件。本页介绍如何使用 AnnotationManagement trait 方法构建标注属性对话框。
事件触发
双击手势由图表内部自动检测(通过 on_mouse_down / on_mouse_up),无需单独调用 on_double_click:
// 在事件循环中处理 on_mouse_up 后,轮询事件队列:
while let Some(event) = cv.poll_event() {
match event {
ChartEvent::DoubleClick(ChartElement::Annotation(id)) => {
open_properties_dialog(id);
}
_ => {}
}
}API 参考
| 方法 | 返回值 | 描述 |
|---|---|---|
cv.annotation_properties(id) | Option<Vec<PropertyDescriptor>> | 本地化描述符列表。id 不存在时返回 None。 |
cv.get_annotation_property(id, name) | Option<PropertyValueResult> | 按路径读取单个属性。 |
cv.set_annotation_property(id, name, value) | Result<(), ChartPropertyError> | 写入属性值。触发重新渲染并推送 AnnotationUpdated。 |
cv.reset_annotation_to_default(id) | () | 将所有样式属性重置为出厂默认值。 |
PropertyDescriptor
pub struct PropertyDescriptor {
pub name: String, // 与 get/set_annotation_property 配合使用的稳定路径键
pub display_name: String, // 本地化标签——直接渲染
pub kind: PropertyKind, // 决定渲染哪种控件
pub enableable: bool, // 为 true 时,在标签旁渲染一个复选框开关
pub category: PropertyCategory, // 该属性所属的设置 Tab
pub tooltip: Option<String>, // 本地化提示文本,或 None
}渲染规则:始终根据 descriptor.kind 选择控件。 不要根据 descriptor.name 做分支判断。
tooltip 为 Some 时,在标签旁渲染一个信息图标(ⓘ),悬停后显示提示内容。
PropertyCategory
PropertyCategory 控制属性出现在哪个设置 Tab:
pub enum PropertyCategory {
Style, // 默认——视觉样式属性(颜色、宽度、线型等)
Inputs, // 计算输入参数(数据源、模式、乘数等)
}构建对话框时按 category 分组:Style 类属性放在样式 Tab,Inputs 类属性放在参数 Tab。某个 Tab 下无属性时不显示该 Tab。
PropertyKind 参考
Fill
单个填充颜色。
Value: PropertyValue::Color(color)
Widget: 颜色选择器Stroke { flags: StrokeFlags }
描边捆绑包——颜色、宽度和/或线型,由 flags 控制:
| 标志 | 含义 |
|---|---|
StrokeFlags::COLOR | 显示颜色选择器 |
StrokeFlags::WIDTH | 显示宽度输入框 |
StrokeFlags::STYLE | 显示线型按钮(实线 / 虚线 / 点线) |
当三个标志全部设置时,捆绑包渲染为一个 LineStylePopover (颜色色块 + 宽度下拉框 + 样式切换按钮)。部分标志时,对应控件内联显示。
Value (enableable: false): PropertyValue::Stroke(stroke)
Value (enableable: true): PropertyValue::Enableable { enabled, value: Stroke(..) }
Widget: LineStylePopover(全部标志)或内联控件(部分标志)pub struct Stroke {
pub color: Color,
pub width: f64,
pub style: LineStyle, // Solid | Dashed | Dotted
}TextStyle { flags: TextStyleFlags }
文字样式捆绑包,由 flags 控制:
| 标志 | 含义 |
|---|---|
TextStyleFlags::COLOR | 显示颜色选择器 |
TextStyleFlags::SIZE | 显示字号选择器 |
TextStyleFlags::BOLD | 显示 B 切换按钮 |
TextStyleFlags::ITALIC | 显示 I 切换按钮 |
四个标志全部设置时→完整格式栏。仅设置 SIZE 时→单个字号下拉框 (用于 FibRetracement 的全局字号)。
Value (enableable: false): PropertyValue::TextStyle(style)
Value (enableable: true): PropertyValue::Enableable { enabled, value: TextStyle(..) }
Widget: 格式栏(颜色 + 字号 + B + I,按标志控制)pub struct TextStyle {
pub color: Color,
pub font_size: f64,
pub bold: bool,
pub italic: bool,
}TextAlign
水平对齐。
Value: PropertyValue::TextAlign(align) // Left | Center | Right
Widget: 3 段按钮(⇤ ↔ ⇥)TextVAlign { options }
垂直对齐。选项已本地化——渲染 option.display_name。
Value: PropertyValue::Enum(index)
Widget: 下拉框Bool
单个切换开关。
Value: PropertyValue::Bool(b)
Widget: 复选框Enum { options }
从固定列表中选择。选项已本地化。
Value: PropertyValue::Enum(index)
Widget: 下拉框Text
多行字符串内容。
Value: PropertyValue::Text(s)
Widget: 文本域Flags { options }
任意位掩码。选项已本地化。第 N 位对应 options[N]。 也用于 Extend(左 / 右),该功能在旧版本中是独立的 kind。
Value: PropertyValue::Int(mask)
Widget: 摘要标签 → 弹出窗口含复选列表// 切换第 N 位
let mut bits = read_int_value(&values, &d.name);
bits ^= 1 << n;
cv.set_annotation_property(id, &d.name, PropertyValue::Int(bits))?;Alpha
不透明度,范围 [0.0, 1.0]。
Value: PropertyValue::Float(f)
Widget: 不透明度滑块Group { has_number: bool, items: Vec<PropertyDescriptor> }
同类行的列表——每个 items 条目对应一行。示例:ParallelChannel 水平线、Pitchfork 层级、FibRetracement 层级。
Value: PropertyValue::GroupItems(Vec<GroupItemValue>)
Widget: 垂直堆叠的行;超过 10 行时→两列网格每行渲染:
| 列 | 显示条件 |
|---|---|
| 复选框 | items[i].enableable == true |
| 数字输入框 | group.has_number == true(如百分比 / 比率) |
| 条目控件 | 始终显示——由 items[i].kind 决定 |
pub struct GroupItemValue {
pub enabled: bool, // 当 items[i].enableable 时有意义
pub number: f64, // 当 group.has_number 时有意义
pub value: Box<PropertyValue>, // 与 items[i].kind 匹配
}路径寻址:
| 路径 | 寻址目标 |
|---|---|
"levels" | 整个 PropertyValue::GroupItems |
"levels.0" | 条目 0 的 PropertyValue::GroupItem(GroupItemValue) |
"levels.0.enabled" | PropertyValue::Bool — 条目 0 的启用标志 |
"levels.0.number" | PropertyValue::Float — 条目 0 的数值 |
"levels.0.color" | 委托给条目 0 的 kind 的子字段 |
enableable — 复选框开关
当 descriptor.enableable 为 true 时,在标签前渲染一个复选框。 禁用时右侧控件变灰(但不隐藏),以便用户重新启用属性时保留原有值。
值包裹在 PropertyValue::Enableable 中:
pub struct EnableableValue {
pub enabled: bool,
pub value: Box<PropertyValue>, // 内部值,如 Stroke 或 TextStyle
}读取:
fn read_stroke_enabled(
values: &HashMap<String, PropertyValueResult>,
d: &PropertyDescriptor,
) -> (bool, Option<Stroke>) {
match values.get(&d.name) {
Some(PropertyValueResult::Value(PropertyValue::Enableable(e))) => {
let stroke = match e.value.as_ref() {
PropertyValue::Stroke(s) => Some(s.clone()),
_ => None,
};
(e.enabled, stroke)
}
Some(PropertyValueResult::Value(PropertyValue::Stroke(s))) => (true, Some(s.clone())),
_ => (true, None),
}
}写入(切换启用状态):
fn set_enableable_enabled(
cv: &impl AnnotationManagement,
id: AnnotationId,
d: &PropertyDescriptor,
values: &HashMap<String, PropertyValueResult>,
enabled: bool,
) -> Result<(), ChartPropertyError> {
let inner = match values.get(&d.name) {
Some(PropertyValueResult::Value(PropertyValue::Enableable(e))) => {
*e.value.clone()
}
Some(PropertyValueResult::Value(v)) => v.clone(),
_ => PropertyValue::Stroke(Stroke::default()),
};
cv.set_annotation_property(
id, &d.name,
PropertyValue::Enableable(EnableableValue { enabled, value: Box::new(inner) }),
)
}PropertyValue 汇总
pub enum PropertyValue {
Color(Color), // Fill、Stroke.color 子字段
Float(f64), // Alpha、number 子字段
Bool(bool), // Bool、enabled 子字段
LineStyle(LineStyle), // Stroke.style 子字段
TextAlign(TextAlign), // TextAlign
Enum(usize), // Enum、TextVAlign
Text(String), // Text
Int(i32), // Flags(位掩码)
Stroke(Stroke), // Stroke(enableable: false)
TextStyle(TextStyle), // TextStyle(enableable: false)
Enableable(EnableableValue), // 任意 kind 且 enableable: true
GroupItems(Vec<GroupItemValue>), // Group(整个列表)
GroupItem(GroupItemValue), // Group(单个条目)
}颜色编码:Color 是 RGBA 格式的打包 u32。 使用 Color::from_rgba(r, g, b, a) 构造,使用 .red() / .green() / .blue() / .alpha() 分解。
读取属性值
fn load_values(
cv: &impl AnnotationManagement,
id: AnnotationId,
descriptors: &[PropertyDescriptor],
) -> HashMap<String, PropertyValueResult> {
descriptors
.iter()
.filter_map(|d| {
let result = cv.get_annotation_property(id, &d.name)?;
Some((d.name.clone(), result))
})
.collect()
}PropertyValueResult:
pub enum PropertyValueResult {
Value(PropertyValue), // 具体值
None, // 属性不适用
Mixed, // 多选时值不同
}对于 None 和 Mixed,以中性/不确定状态渲染控件。
写入属性值
标量属性:
// 切换 TextStyle 的粗体
cv.set_annotation_property(id, "textStyle.bold", PropertyValue::Bool(true))?;
// 设置 Flags 位掩码(向右延伸)
cv.set_annotation_property(id, "extend", PropertyValue::Int(0b10))?;修改 Stroke 捆绑包:
fn patch_stroke_color(
cv: &impl AnnotationManagement,
id: AnnotationId,
d: &PropertyDescriptor,
values: &HashMap<String, PropertyValueResult>,
new_color: Color,
) -> Result<(), ChartPropertyError> {
let default = Stroke { color: Color::from_rgba(0, 0, 0, 255), width: 1.0, style: LineStyle::Solid };
let (enabled, stroke) = if d.enableable {
match values.get(&d.name) {
Some(PropertyValueResult::Value(PropertyValue::Enableable(e))) => (
e.enabled,
match e.value.as_ref() { PropertyValue::Stroke(s) => s.clone(), _ => default },
),
_ => (true, default),
}
} else {
let s = match values.get(&d.name) {
Some(PropertyValueResult::Value(PropertyValue::Stroke(s))) => s.clone(),
_ => default,
};
(true, s)
};
let next_stroke = Stroke { color: new_color, ..stroke };
let value = if d.enableable {
PropertyValue::Enableable(EnableableValue {
enabled,
value: Box::new(PropertyValue::Stroke(next_stroke)),
})
} else {
PropertyValue::Stroke(next_stroke)
};
cv.set_annotation_property(id, &d.name, value)
}修改 Group 条目:
fn patch_group_item_color(
cv: &impl AnnotationManagement,
id: AnnotationId,
d: &PropertyDescriptor,
values: &HashMap<String, PropertyValueResult>,
idx: usize,
new_color: Color,
) -> Result<(), ChartPropertyError> {
let items = match values.get(&d.name) {
Some(PropertyValueResult::Value(PropertyValue::GroupItems(v))) => v.clone(),
_ => return Ok(()),
};
let mut items = items;
if let Some(item) = items.get_mut(idx) {
if let PropertyValue::Stroke(ref mut s) = *item.value {
s.color = new_color;
}
}
cv.set_annotation_property(id, &d.name, PropertyValue::GroupItems(items))
}对话框结构
参数标签页
当任意描述符的 category = PropertyCategory::Inputs 时显示。按顺序渲染所有 Inputs 类描述符,格式与样式行相同。
该标签页用于带计算参数的标注——数据源、计算模式、乘数等。
样式标签页
渲染所有 category = PropertyCategory::Style 的非文字类描述符(排除 Text、TextStyle、TextAlign、TextVAlign)。 按顺序遍历描述符列表,依次渲染每个描述符为带标签的行。
行布局:
enableable: false→ 左侧为[标签],右侧为控件。enableable: true→ 左侧为[☑ 标签](复选框 + 标签合并),禁用时右侧控件变灰。
文字标签页
当任意描述符的 kind = Text 时显示。包含:
| 分区 | 描述符 |
|---|---|
| 格式栏 | TextStyle — 颜色 + 字号 + B + I(按标志控制) |
| 内容 | Text — 多行文本域 |
| 对齐 | TextAlign + TextVAlign |
坐标标签页
当标注有控制点时显示。使用 annotation(id) 获取控制点,使用 annotation_control_point_flags(id) 获取每个点的轴向锁定标志:
use navi_chart::{AnnotationManagement, annotation::ControlPointFlags};
if let Some(annotation) = cv.annotation(id) {
let flags_per_point = cv.annotation_control_point_flags(id).unwrap_or_default();
for (i, point) in annotation.spec.points.iter().enumerate() {
let flags = flags_per_point.get(i).copied().unwrap_or_default();
if !flags.contains(ControlPointFlags::LOCK_BAR_INDEX) {
render_bar_index_input(i, point.time);
}
if !flags.contains(ControlPointFlags::LOCK_PRICE) {
render_price_input(i, point.price);
}
}
}每个点最多渲染两个数字输入框(bar 索引 + 价格)。当对应锁定标志被设置时隐藏输入框:
| 标志 | 位值 | 效果 |
|---|---|---|
ControlPointFlags::LOCK_BAR_INDEX | 0b0001 | 隐藏 bar 索引输入框 |
ControlPointFlags::LOCK_PRICE | 0b0010 | 隐藏价格输入框 |
保持对话框同步
ChartEvent::AnnotationUpdated(updated_id) if updated_id == dialog_id => {
values = load_values(&cv, dialog_id, &descriptors);
}
ChartEvent::AnnotationDeleted(deleted_id) if deleted_id == dialog_id => {
close_dialog();
}参考实现
参见 Web Playground 中的 AnnotationPropertiesDialog.vue,了解完整的前端实现。