Skip to content

标注属性对话框

当用户双击图表上的标注时,图表内部会自动识别双击手势并推送 ChartEvent::DoubleClick(ChartElement::Annotation(id)) 事件。本页介绍如何使用 AnnotationManagement trait 方法构建标注属性对话框。

事件触发

双击手势由图表内部自动检测(通过 on_mouse_down / on_mouse_up),无需单独调用 on_double_click

rust
// 在事件循环中处理 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

rust
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 做分支判断。

tooltipSome 时,在标签旁渲染一个信息图标(ⓘ),悬停后显示提示内容。

PropertyCategory

PropertyCategory 控制属性出现在哪个设置 Tab:

rust
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(全部标志)或内联控件(部分标志)
rust
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,按标志控制)
rust
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: 摘要标签 → 弹出窗口含复选列表
rust
// 切换第 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 决定
rust
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.enableabletrue 时,在标签前渲染一个复选框。 禁用时右侧控件变灰(但不隐藏),以便用户重新启用属性时保留原有值。

值包裹在 PropertyValue::Enableable 中:

rust
pub struct EnableableValue {
    pub enabled: bool,
    pub value: Box<PropertyValue>,  // 内部值,如 Stroke 或 TextStyle
}

读取:

rust
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),
    }
}

写入(切换启用状态):

rust
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 汇总

rust
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() 分解。

读取属性值

rust
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

rust
pub enum PropertyValueResult {
    Value(PropertyValue),  // 具体值
    None,                  // 属性不适用
    Mixed,                 // 多选时值不同
}

对于 NoneMixed,以中性/不确定状态渲染控件。

写入属性值

标量属性:

rust
// 切换 TextStyle 的粗体
cv.set_annotation_property(id, "textStyle.bold", PropertyValue::Bool(true))?;

// 设置 Flags 位掩码(向右延伸)
cv.set_annotation_property(id, "extend", PropertyValue::Int(0b10))?;

修改 Stroke 捆绑包:

rust
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 条目:

rust
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 的非文字类描述符(排除 TextTextStyleTextAlignTextVAlign)。 按顺序遍历描述符列表,依次渲染每个描述符为带标签的行。

行布局

  • enableable: false → 左侧为 [标签],右侧为控件。
  • enableable: true → 左侧为 [☑ 标签](复选框 + 标签合并),禁用时右侧控件变灰。

文字标签页

当任意描述符的 kind = Text 时显示。包含:

分区描述符
格式栏TextStyle — 颜色 + 字号 + B + I(按标志控制)
内容Text — 多行文本域
对齐TextAlign + TextVAlign

坐标标签页

当标注有控制点时显示。使用 annotation(id) 获取控制点,使用 annotation_control_point_flags(id) 获取每个点的轴向锁定标志:

rust
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_INDEX0b0001隐藏 bar 索引输入框
ControlPointFlags::LOCK_PRICE0b0010隐藏价格输入框

保持对话框同步

rust
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,了解完整的前端实现。

基于 MIT 许可证发布。