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 许可证发布。