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}