標注屬性對話框
當用戶雙擊圖表上的標注時,圖表內部會自動識別雙擊手勢並推送 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,了解完整的前端實現。