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 許可證發佈。