选中样式工具栏
当用户在图表上选中标注时,选中项附近会出现一个浮动工具栏。本页介绍如何使用与标注属性对话框相同的 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 含 top 和 bottom 两条描边), 将写入应用到分组中的所有描述符:
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,了解完整的前端实现。