Annotation Properties Dialog
When the user double-clicks an annotation on the chart, the gesture is detected internally and ChartEvent::DoubleClick(ChartElement::Annotation(id)) is pushed. This page covers how to build the resulting properties dialog using the AnnotationManagement trait methods.
Event Trigger
// Double-click is detected automatically from on_mouse_down / on_mouse_up.
// Just drain the event queue after each interaction:
while let Some(event) = cv.poll_event() {
match event {
ChartEvent::DoubleClick(ChartElement::Annotation(id)) => {
open_properties_dialog(id);
}
_ => {}
}
}API Reference
| Method | Returns | Description |
|---|---|---|
cv.annotation_properties(id) | Option<Vec<PropertyDescriptor>> | Localised descriptor list. None if id not found. |
cv.get_annotation_property(id, name) | Option<PropertyValueResult> | Read one property by path. |
cv.set_annotation_property(id, name, value) | Result<(), ChartPropertyError> | Write a property value. Triggers re-render and pushes AnnotationUpdated. |
cv.reset_annotation_to_default(id) | () | Reset all style properties to factory defaults. |
PropertyDescriptor
pub struct PropertyDescriptor {
pub name: String, // Stable path key for get/set_annotation_property
pub display_name: String, // Localised label — render verbatim
pub kind: PropertyKind, // Drives which widget to render
pub enableable: bool, // When true, render a checkbox gate beside the label
pub category: PropertyCategory, // Which tab this property belongs to
pub tooltip: Option<String>, // Localised tooltip text, or None
}Rendering rule: always dispatch on descriptor.kind to choose the widget. Never branch on descriptor.name.
When descriptor.tooltip is Some, render an info icon (ⓘ) next to the label. Hovering over it shows the tooltip text in a popover.
PropertyCategory
PropertyCategory controls which settings tab a property appears in:
pub enum PropertyCategory {
Style, // Default — visual properties (colour, width, line style, …)
Inputs, // Calculation inputs (source, mode, multipliers, …)
}Group descriptors by category when building the dialog. Properties with category = Style go in the Style tab; properties with category = Inputs go in the Inputs tab. Only show a tab when at least one property belongs to it.
PropertyKind Reference
Fill
A single fill colour.
Value: PropertyValue::Color(color)
Widget: colour pickerStroke { flags: StrokeFlags }
A stroke bundle — colour, width, and/or line style, controlled by flags:
| Flag | Meaning |
|---|---|
StrokeFlags::COLOR | Colour picker visible |
StrokeFlags::WIDTH | Width input visible |
StrokeFlags::STYLE | Line-style buttons (solid / dashed / dotted) visible |
When all three flags are set the bundle renders as a single LineStylePopover (colour swatch + width dropdown + style toggle). With partial flags, the corresponding controls appear inline.
Value (enableable: false): PropertyValue::Stroke(stroke)
Value (enableable: true): PropertyValue::Enableable { enabled, value: Stroke(..) }
Widget: LineStylePopover (all flags) or inline controls (partial flags)pub struct Stroke {
pub color: Color,
pub width: f64,
pub style: LineStyle, // Solid | Dashed | Dotted
}TextStyle { flags: TextStyleFlags }
A text-style bundle controlled by flags:
| Flag | Meaning |
|---|---|
TextStyleFlags::COLOR | Colour picker visible |
TextStyleFlags::SIZE | Font-size selector visible |
TextStyleFlags::BOLD | B toggle visible |
TextStyleFlags::ITALIC | I toggle visible |
All four set → full format bar. SIZE only → single font-size dropdown (used by FibRetracement's global font size).
Value (enableable: false): PropertyValue::TextStyle(style)
Value (enableable: true): PropertyValue::Enableable { enabled, value: TextStyle(..) }
Widget: format bar (colour + size + B + I, per flags)pub struct TextStyle {
pub color: Color,
pub font_size: f64,
pub bold: bool,
pub italic: bool,
}TextAlign
Horizontal alignment.
Value: PropertyValue::TextAlign(align) // Left | Center | Right
Widget: 3-segment button (⇤ ↔ ⇥)TextVAlign { options }
Vertical alignment. Options are localised — render option.display_name.
Value: PropertyValue::Enum(index)
Widget: dropdownBool
A single toggle.
Value: PropertyValue::Bool(b)
Widget: checkboxEnum { options }
A choice from a fixed list. Options are localised.
Value: PropertyValue::Enum(index)
Widget: dropdownText
Multi-line string content.
Value: PropertyValue::Text(s)
Widget: textareaFlags { options }
An arbitrary bitmask. Options are localised. Bit N corresponds to options[N]. Also used for Extend (Left / Right), which was a dedicated kind in older versions.
Value: PropertyValue::Int(mask)
Widget: summary label → popover with checklist// Toggle bit N
let mut bits = read_int_value(&values, &d.name);
bits ^= 1 << n;
cv.set_annotation_property(id, &d.name, PropertyValue::Int(bits))?;Alpha
Opacity in [0.0, 1.0].
Value: PropertyValue::Float(f)
Widget: opacity sliderGroup { has_number: bool, items: Vec<PropertyDescriptor> }
A list of homogeneous rows — one per items entry. Examples: ParallelChannel level lines, Pitchfork levels, FibRetracement levels.
Value: PropertyValue::GroupItems(Vec<GroupItemValue>)
Widget: vertically stacked rows; >10 items → two-column gridEach row renders:
| Column | Shown when |
|---|---|
| Checkbox | items[i].enableable == true |
| Number input | group.has_number == true (e.g. percent / ratio) |
| Item control | Always — driven by items[i].kind |
pub struct GroupItemValue {
pub enabled: bool, // meaningful when items[i].enableable
pub number: f64, // meaningful when group.has_number
pub value: Box<PropertyValue>, // matches items[i].kind
}Path addressing:
| Path | Addresses |
|---|---|
"levels" | Whole PropertyValue::GroupItems |
"levels.0" | PropertyValue::GroupItem(GroupItemValue) for item 0 |
"levels.0.enabled" | PropertyValue::Bool — item 0 enabled flag |
"levels.0.number" | PropertyValue::Float — item 0 number |
"levels.0.color" | Sub-field delegated to item 0's kind |
enableable — Checkbox Gate
When descriptor.enableable is true, render a checkbox before the label. The controls to the right are dimmed (but not hidden) when disabled, so the underlying values are preserved when the user re-enables the property.
The value is wrapped in PropertyValue::Enableable:
pub struct EnableableValue {
pub enabled: bool,
pub value: Box<PropertyValue>, // the inner value, e.g. Stroke or TextStyle
}Reading:
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),
}
}Writing (toggle enabled):
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 Summary
pub enum PropertyValue {
Color(Color), // Fill, Stroke.color sub-field
Float(f64), // Alpha, number sub-fields
Bool(bool), // Bool, enabled sub-fields
LineStyle(LineStyle), // Stroke.style sub-field
TextAlign(TextAlign), // TextAlign
Enum(usize), // Enum, TextVAlign
Text(String), // Text
Int(i32), // Flags (bitmask)
Stroke(Stroke), // Stroke (enableable: false)
TextStyle(TextStyle), // TextStyle (enableable: false)
Enableable(EnableableValue), // Any kind with enableable: true
GroupItems(Vec<GroupItemValue>), // Group (whole list)
GroupItem(GroupItemValue), // Group (single item)
}Color encoding: Color is a packed u32 in RGBA order. Use Color::from_rgba(r, g, b, a) to construct and .red() / .green() / .blue() / .alpha() to decompose.
Reading Property Values
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), // Concrete value
None, // Property not applicable
Mixed, // Multi-selection with differing values
}Render widgets in a neutral/indeterminate state for None and Mixed.
Writing Property Values
Scalar property:
// Toggle bold on a TextStyle
cv.set_annotation_property(id, "textStyle.bold", PropertyValue::Bool(true))?;
// Set a Flags bitmask (extend right)
cv.set_annotation_property(id, "extend", PropertyValue::Int(0b10))?;Patching a Stroke bundle:
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)
}Patching a Group item:
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))
}Dialog Structure
Inputs Tab
Shown when any descriptor has category = PropertyCategory::Inputs. Renders all Inputs descriptors in order, as labelled rows (same layout as Style rows).
This tab is used by annotations with calculation parameters — source price, band mode, multipliers, etc.
Style Tab
Renders all descriptors with category = PropertyCategory::Style that are not text-family (Text, TextStyle, TextAlign, TextVAlign). Walk the descriptor list in order and render each one as a labelled row.
Row layout:
enableable: false→[Label]on the left, control on the right.enableable: true→[☑ Label](checkbox + label combined) on the left; controls on the right are dimmed when disabled.
Text Tab
Shown when any descriptor has kind = Text. Contains:
| Section | Descriptors |
|---|---|
| Format bar | TextStyle — colour + size + B + I (per flags) |
| Content | Text — multi-line textarea |
| Alignment | TextAlign + TextVAlign |
Coordinates Tab
Shown when the annotation has control points. Use annotation(id) to get the control points and annotation_control_point_flags(id) to get per-point axis lock flags:
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);
}
}
}Each point renders up to two number inputs (bar index + price). Hide an input when the corresponding lock flag is set:
| Flag | Bit | Effect |
|---|---|---|
ControlPointFlags::LOCK_BAR_INDEX | 0b0001 | Hide bar-index input |
ControlPointFlags::LOCK_PRICE | 0b0010 | Hide price input |
Keeping the Dialog in Sync
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();
}Reference Implementation
See AnnotationPropertiesDialog.vue in the web playground for a complete frontend implementation.