Skip to content

選中樣式工具列

當用戶在圖表上選中標注時,選中項附近會出現一個浮動工具列。本頁介紹如何使用與標注屬性對話框相同的 AnnotationManagement trait 方法構建該工具列。

樣式工具列佈局樣式工具列佈局

概述

工具列是一個水平條,提供對選中標注最常用視覺屬性的快速訪問控件。與覆蓋所有描述符的完整屬性對話框不同,工具列按視覺角色對描述符分組——描邊顏色、線型、線寬、填充顏色、文字樣式——即使有多個描述符適用(如具有 7 條水平線的 ParallelChannel),每組也只渲染一個共享控件。

觸發工具列

監聽 ChartEvent::SelectionChanged

rust
ChartEvent::SelectionChanged(Some(ChartElement::Annotation(id))) => {
    let descriptors = cv.annotation_properties(id).unwrap_or_default();
    let values = load_values(&cv, id, &descriptors);
    show_toolbar(id, &descriptors, &values, cv.selected_annotation_bounds());
}
ChartEvent::SelectionChanged(None) => {
    hide_toolbar();
}

使用 cv.selected_annotation_bounds() 定位工具列,該方法返回畫布像素坐標中的邊界框 Option<Rect>

分組描述符

按每個描述符所貢獻的視覺角色對描述符列表分組。單個 Stroke 描述符(三個標誌全部設置)會同時貢獻到三個描邊槽位。

rust
#[derive(Default)]
struct ToolbarGroups<'a> {
    stroke_colors: Vec<&'a PropertyDescriptor>,
    line_widths:   Vec<&'a PropertyDescriptor>,
    line_styles:   Vec<&'a PropertyDescriptor>,
    fill_colors:   Vec<&'a PropertyDescriptor>,
    text_styles:   Vec<&'a PropertyDescriptor>,
}

fn group_descriptors<'a>(descriptors: &'a [PropertyDescriptor]) -> ToolbarGroups<'a> {
    let mut g = ToolbarGroups::default();
    for d in descriptors {
        match &d.kind {
            // 填充顏色
            PropertyKind::Fill => g.fill_colors.push(d),

            // 描邊——標誌控制該描述符貢獻到哪些槽位
            PropertyKind::Stroke { flags } => {
                if flags.contains(StrokeFlags::COLOR) { g.stroke_colors.push(d); }
                if flags.contains(StrokeFlags::WIDTH) { g.line_widths.push(d); }
                if flags.contains(StrokeFlags::STYLE) { g.line_styles.push(d); }
            }

            // 文字樣式——始終貢獻到文字樣式槽位
            PropertyKind::TextStyle { .. } => g.text_styles.push(d),

            // Group(如 ParallelChannel 水平線、Pitchfork 層級)
            // ——為工具列目的視為多個描邊描述符
            PropertyKind::Group { items, .. } => {
                // 使用 group 描述符本身作為代理;工具列
                // 通過 GroupItems 寫入所有條目。
                // 檢查第一個條目的 kind 以確定適用哪些槽位。
                if let Some(first) = items.first() {
                    if let PropertyKind::Stroke { flags } = &first.kind {
                        if flags.contains(StrokeFlags::COLOR) { g.stroke_colors.push(d); }
                        if flags.contains(StrokeFlags::WIDTH) { g.line_widths.push(d); }
                        if flags.contains(StrokeFlags::STYLE) { g.line_styles.push(d); }
                    }
                }
            }

            _ => {}
        }
    }
    g
}

僅當對應分組非空時才顯示工具列槽位。

讀取描邊子字段

從任意描述符中提取相關子字段用於顯示:

rust
enum StrokeField { Color, Width, Style }

fn read_stroke_field(
    d: &PropertyDescriptor,
    values: &HashMap<String, PropertyValueResult>,
    field: StrokeField,
) -> Option<PropertyValue> {
    let pv = match values.get(&d.name)? {
        PropertyValueResult::Value(v) => v,
        _ => return None,
    };

    // 如果存在 Enableable 包裝,則解包
    let inner = match pv {
        PropertyValue::Enableable(e) => e.value.as_ref(),
        other => other,
    };

    // 對於 Group,檢查條目 0
    let stroke = match inner {
        PropertyValue::Stroke(s) => s,
        PropertyValue::GroupItems(items) => {
            let item_value = match items.first()?.value.as_ref() {
                PropertyValue::Stroke(s) => s,
                _ => return None,
            };
            item_value
        }
        _ => return None,
    };

    Some(match field {
        StrokeField::Color => PropertyValue::Color(stroke.color),
        StrokeField::Width => PropertyValue::Float(stroke.width),
        StrokeField::Style => PropertyValue::LineStyle(stroke.style),
    })
}

當多個描述符共享同一槽位時(如 DisjointChannel 的兩條描邊),使用第一個返回具體值的描述符。

寫入描邊子字段

修改捆綁包值,只寫入變化的字段:

rust
fn write_stroke_field(
    cv: &impl AnnotationManagement,
    id: AnnotationId,
    d: &PropertyDescriptor,
    values: &HashMap<String, PropertyValueResult>,
    field: StrokeField,
    new_val: PropertyValue,
) -> Result<(), ChartPropertyError> {
    let default_stroke = Stroke { color: Color::from_rgba(0, 0, 0, 255), width: 1.0, style: LineStyle::Solid };

    match &d.kind {
        PropertyKind::Stroke { .. } => {
            let (enabled, mut stroke) = read_stroke_with_enabled(values, d, &default_stroke);
            apply_stroke_field(&mut stroke, field, new_val);
            let value = if d.enableable {
                PropertyValue::Enableable(EnableableValue {
                    enabled,
                    value: Box::new(PropertyValue::Stroke(stroke)),
                })
            } else {
                PropertyValue::Stroke(stroke)
            };
            cv.set_annotation_property(id, &d.name, value)
        }

        PropertyKind::Group { .. } => {
            // 將更改應用到 group 中的每個條目
            let items = match values.get(&d.name) {
                Some(PropertyValueResult::Value(PropertyValue::GroupItems(v))) => v.clone(),
                _ => return Ok(()),
            };
            let items: Vec<GroupItemValue> = items
                .into_iter()
                .map(|mut item| {
                    if let PropertyValue::Stroke(ref mut s) = *item.value {
                        apply_stroke_field(s, field, new_val.clone());
                    }
                    item
                })
                .collect();
            cv.set_annotation_property(id, &d.name, PropertyValue::GroupItems(items))
        }

        _ => Ok(()),
    }
}

fn apply_stroke_field(s: &mut Stroke, field: StrokeField, val: PropertyValue) {
    match (field, val) {
        (StrokeField::Color, PropertyValue::Color(c))     => s.color = c,
        (StrokeField::Width, PropertyValue::Float(w))     => s.width = w,
        (StrokeField::Style, PropertyValue::LineStyle(ls)) => s.style = ls,
        _ => {}
    }
}

對於多描述符槽位(如 FlatTopBottom 含 topbottom 兩條描邊), 將寫入應用到分組中的所有描述符:

rust
fn apply_to_group(
    cv: &impl AnnotationManagement,
    id: AnnotationId,
    group: &[&PropertyDescriptor],
    values: &HashMap<String, PropertyValueResult>,
    field: StrokeField,
    new_val: PropertyValue,
) {
    for d in group {
        let _ = write_stroke_field(cv, id, d, values, field, new_val.clone());
    }
}

工具列控件

槽位顯示條件控件寫入方式
描邊顏色stroke_colors 非空顏色色塊 → 選擇器StrokeField::Color 應用到分組所有描述符
線型line_styles 非空3 按鈕切換:實線 / 虛線 / 點線StrokeField::Style 應用到分組所有描述符
線寬line_widths 非空按鈕組(1 / 2 / 3 / 4 px)StrokeField::Width 應用到分組所有描述符
填充顏色fill_colors 非空顏色色塊 → 選擇器PropertyValue::Color
文字顏色text_styles 非空顏色色塊 → 選擇器修改 TextStyle.color
粗體text_styles 非空切換 B修改 TextStyle.bold
斜體text_styles 非空切換 I修改 TextStyle.italic
字號text_styles 非空下拉框(8 / 10 / 12 / 14 / 16 / 20 / 24)修改 TextStyle.font_size
刪除始終顯示垃圾桶圖示cv.delete_selected_annotation()
更多(⋯)可選打開完整屬性對話框

讀取文字樣式子字段

rust
fn read_text_style_field(
    d: &PropertyDescriptor,
    values: &HashMap<String, PropertyValueResult>,
) -> Option<TextStyle> {
    match values.get(&d.name)? {
        PropertyValueResult::Value(PropertyValue::TextStyle(ts)) => Some(ts.clone()),
        PropertyValueResult::Value(PropertyValue::Enableable(e)) => {
            match e.value.as_ref() {
                PropertyValue::TextStyle(ts) => Some(ts.clone()),
                _ => None,
            }
        }
        _ => None,
    }
}

刷新工具列

在每次 AnnotationUpdated 事件時重新讀取描述符值,以與屬性對話框的更改保持同步:

rust
ChartEvent::AnnotationUpdated(id) if id == selected_id => {
    values = load_values(&cv, id, &descriptors);
    refresh_toolbar(&values);
}
ChartEvent::SelectionChanged(_) => {
    // 每次選擇變更時重新運行完整的 show_toolbar 邏輯
}

參考實現

參見 Web Playground 中的 SelectionToolbar.vue,了解完整的前端實現。

基於 MIT 許可證發佈。