Skip to main content

damascene_core/scene/
labels.rs

1//! Per-point text labels for scatter marks: persistent annotations and
2//! hover tooltips.
3//!
4//! Like [`axes`](crate::scene::axes), this is *presentation*: the text and
5//! how to show it live on the mark ([`PointDraw`](crate::scene::PointDraw)),
6//! not in the uploaded geometry ([`ScenePoint`](crate::scene::ScenePoint)
7//! stays a pure `repr(C)` GPU vertex). The draw-op pass projects the labels
8//! through the resolved camera and emits text — persistent labels reuse the
9//! shared `scene_label` seam (depth-occluded like axis labels), hover
10//! tooltips pick the point under the cursor and draw a styled chip.
11//!
12//! Modelled on the plotly scatter `text` / `hovertext` / `textposition`
13//! vocabulary — the de-facto paradigm for labelled scatter data.
14
15use std::sync::Arc;
16
17use crate::color::Color;
18
19/// When a mark's point labels are shown.
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
21pub enum LabelDisplay {
22    /// Persistent: every point with non-empty text is labelled (the app is
23    /// responsible for keeping that sparse enough to read).
24    Always,
25    /// On hover only: the point under the cursor shows its text as a chip.
26    #[default]
27    Hover,
28}
29
30/// The scatter point currently under the cursor, surfaced to the app via
31/// [`BuildCx::hovered_scene_point`](crate::event::BuildCx::hovered_scene_point).
32///
33/// Scene marks have no keys (they're positional), so a point is identified by
34/// the scene node, the mark's index in [`SceneSpec`](crate::scene::SceneSpec)'s
35/// point list, and the point's index within that mark. The app indexes its own
36/// data by `point` to drive a detail panel / highlight / linked view — the 3D
37/// analogue of reading a `PointerEnter` key off a 2D hit-target. Picked from
38/// the same [`LabelDisplay::Hover`] path that draws the built-in tooltip chip,
39/// so it honours the same depth-occlusion and behind-camera culling.
40#[derive(Clone, Debug, PartialEq, Eq)]
41pub struct ScenePointPick {
42    /// `computed_id` of the `Scene3D` node the point belongs to.
43    pub scene: String,
44    /// Index of the point mark within the scene's point list.
45    pub mark: usize,
46    /// Index of the point within that mark's geometry.
47    pub point: usize,
48}
49
50/// Where a label sits relative to its point's projected screen position.
51/// The gap from the marker is derived from the point size.
52#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
53pub enum LabelPlacement {
54    /// Centred on the point (axis labels use this).
55    Center,
56    /// Above the marker — the usual scatter-label spot.
57    #[default]
58    Above,
59    Below,
60    Left,
61    Right,
62}
63
64/// Per-point labels for a scatter mark.
65///
66/// `text` is parallel to the mark's points; an empty string (or an index
67/// past the end) means "no label". Build with [`PointLabels::new`] and the
68/// `display` / `placement` / styling setters.
69#[derive(Clone, Debug, PartialEq)]
70pub struct PointLabels {
71    /// Label text, indexed alongside the mark's points.
72    pub text: Arc<[String]>,
73    pub display: LabelDisplay,
74    pub placement: LabelPlacement,
75    /// Text colour. `None` uses a theme token (foreground for persistent
76    /// labels, popover-foreground for tooltip chips).
77    pub color: Option<Color>,
78    /// Label font size in logical pixels.
79    pub size: f32,
80}
81
82impl PointLabels {
83    /// Labels from any string iterator, defaulting to hover tooltips.
84    pub fn new(text: impl IntoIterator<Item = impl Into<String>>) -> Self {
85        Self {
86            text: text.into_iter().map(Into::into).collect(),
87            display: LabelDisplay::default(),
88            placement: LabelPlacement::default(),
89            color: None,
90            size: 11.0,
91        }
92    }
93
94    /// Show every labelled point persistently rather than on hover.
95    pub fn always(mut self) -> Self {
96        self.display = LabelDisplay::Always;
97        self
98    }
99
100    /// Show on hover (the default).
101    pub fn on_hover(mut self) -> Self {
102        self.display = LabelDisplay::Hover;
103        self
104    }
105
106    /// Set where persistent labels sit relative to the marker.
107    pub fn placement(mut self, placement: LabelPlacement) -> Self {
108        self.placement = placement;
109        self
110    }
111
112    /// Override the label text colour.
113    pub fn color(mut self, color: Color) -> Self {
114        self.color = Some(color);
115        self
116    }
117
118    /// Set the label font size (logical px).
119    pub fn size(mut self, size: f32) -> Self {
120        self.size = size;
121        self
122    }
123
124    /// The label for point `i`, or `None` when absent / empty.
125    pub fn get(&self, i: usize) -> Option<&str> {
126        self.text
127            .get(i)
128            .map(String::as_str)
129            .filter(|s| !s.is_empty())
130    }
131}