Skip to content

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

rust
// 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

MethodReturnsDescription
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

rust
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:

rust
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 picker

Stroke { flags: StrokeFlags }

A stroke bundle — colour, width, and/or line style, controlled by flags:

FlagMeaning
StrokeFlags::COLORColour picker visible
StrokeFlags::WIDTHWidth input visible
StrokeFlags::STYLELine-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)
rust
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:

FlagMeaning
TextStyleFlags::COLORColour picker visible
TextStyleFlags::SIZEFont-size selector visible
TextStyleFlags::BOLDB toggle visible
TextStyleFlags::ITALICI 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)
rust
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: dropdown

Bool

A single toggle.

Value: PropertyValue::Bool(b)
Widget: checkbox

Enum { options }

A choice from a fixed list. Options are localised.

Value: PropertyValue::Enum(index)
Widget: dropdown

Text

Multi-line string content.

Value: PropertyValue::Text(s)
Widget: textarea

Flags { 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
rust
// 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 slider

Group { 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 grid

Each row renders:

ColumnShown when
Checkboxitems[i].enableable == true
Number inputgroup.has_number == true (e.g. percent / ratio)
Item controlAlways — driven by items[i].kind
rust
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:

PathAddresses
"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:

rust
pub struct EnableableValue {
    pub enabled: bool,
    pub value: Box<PropertyValue>,  // the inner value, e.g. Stroke or TextStyle
}

Reading:

rust
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):

rust
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

rust
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

rust
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:

rust
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:

rust
// 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:

rust
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:

rust
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:

SectionDescriptors
Format barTextStyle — colour + size + B + I (per flags)
ContentText — multi-line textarea
AlignmentTextAlign + 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:

rust
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:

FlagBitEffect
ControlPointFlags::LOCK_BAR_INDEX0b0001Hide bar-index input
ControlPointFlags::LOCK_PRICE0b0010Hide price input

Keeping the Dialog in Sync

rust
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.

Released under the MIT License.