1use crate::{
4 drag::{DragConfig, Draggable},
5 layout::{NudgeReject, NudgeResult, RectF, Side},
6 select::Selectable,
7 text::MeasureText,
8};
9
10#[derive(Clone, Debug, PartialEq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub enum DecorationId {
15 DataArea,
17 DataAreaEdge(Side),
19 ChartTitle,
20 Axis(Side),
21 AxisLabel(Side),
22 AxisTitle(Side),
23 Legend,
24 RadialLabel(crate::radial::RadialTarget),
25 DataLabel {
26 category_id: String,
27 series_id: String,
28 },
29}
30impl DecorationId {
31 pub fn element_id(&self) -> String {
32 let side = |s: &Side| match s {
33 Side::Top => "top",
34 Side::Bottom => "bottom",
35 Side::Left => "left",
36 Side::Right => "right",
37 };
38 match self {
39 Self::DataArea => "data_area".into(),
40 Self::DataAreaEdge(s) => format!("data_area_edge_{}", side(s)),
41 Self::ChartTitle => "chart_title".into(),
42 Self::Axis(s) => format!("axis_{}", side(s)),
43 Self::AxisLabel(s) => format!("tick_labels_{}", side(s)),
44 Self::AxisTitle(s) => format!("axis_title_{}", side(s)),
45 Self::Legend => "legend".into(),
46 Self::RadialLabel(t) => format!("radial_label_{}_{}", t.group, t.index),
47 Self::DataLabel {
48 category_id,
49 series_id,
50 } => format!(
51 "data_label_{}:{}{}:{}",
52 category_id.len(),
53 category_id,
54 series_id.len(),
55 series_id
56 ),
57 }
58 }
59}
60#[derive(Clone, Debug, PartialEq)]
61#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
62pub struct DecorationOffset {
63 pub element: DecorationId,
64 pub offset: [f32; 2],
66}
67#[derive(Clone, Debug, Default, PartialEq)]
68#[cfg_attr(
69 feature = "serde",
70 derive(serde::Serialize, serde::Deserialize),
71 serde(default)
72)]
73pub struct DecorationLayout {
74 pub data_area: Option<RectF>,
77 pub offsets: Vec<DecorationOffset>,
78 pub selected: Option<DecorationId>,
80}
81impl DecorationLayout {
82 pub fn offset(&self, id: &DecorationId) -> [f32; 2] {
83 self.offsets
84 .iter()
85 .find(|o| &o.element == id)
86 .map_or([0.; 2], |o| o.offset)
87 }
88 pub fn effective_offset(&self, id: &DecorationId) -> [f32; 2] {
90 let mut p = self.offset(id);
91 if let DecorationId::AxisLabel(side) = id {
92 let q = self.offset(&DecorationId::Axis(side.clone()));
93 p[0] += q[0];
94 p[1] += q[1];
95 }
96 p
97 }
98 pub fn reset(&mut self, id: &DecorationId) {
99 if *id == DecorationId::DataArea {
100 self.data_area = None;
101 }
102 self.offsets.retain(|o| &o.element != id);
103 }
104 pub fn resolve_data_area(&self, automatic: RectF) -> Result<RectF, &'static str> {
105 let r = self.data_area.unwrap_or(automatic);
106 if ![r.x, r.y, r.width, r.height, r.x + r.width, r.y + r.height]
107 .into_iter()
108 .all(f32::is_finite)
109 || r.x < 0.
110 || r.y < 0.
111 || r.width < 80.
112 || r.height < 60.
113 || r.x < automatic.x
114 || r.y < automatic.y
115 || r.x + r.width > automatic.x + automatic.width
116 || r.y + r.height > automatic.y + automatic.height
117 {
118 return Err(
119 "data area must fit the measured plot region and be at least 80 by 60 logical pixels",
120 );
121 }
122 Ok(r)
123 }
124 pub fn validate(&self) -> Result<(), &'static str> {
125 if let Some(r) = self.data_area {
126 self.resolve_data_area(r)?;
127 }
128 if self.offsets.len() > 1024 {
129 return Err("too many decoration offsets");
130 }
131 for (i, o) in self.offsets.iter().enumerate() {
132 if o.offset.iter().any(|x| !x.is_finite())
133 || self.offsets[..i].iter().any(|p| p.element == o.element)
134 || matches!(
135 o.element,
136 DecorationId::DataArea | DecorationId::DataAreaEdge(_)
137 )
138 {
139 return Err("decoration offsets must be finite and unique");
140 }
141 if let DecorationId::Axis(s) = &o.element {
142 let parallel = match s {
143 Side::Top | Side::Bottom => o.offset[0],
144 _ => o.offset[1],
145 };
146 if parallel != 0.0 {
147 return Err("axes only move perpendicular to their direction");
148 }
149 }
150 }
151 Ok(())
152 }
153}
154#[derive(Clone, Debug)]
157pub struct DecorationElement {
158 pub id: DecorationId,
159 pub base_bounds: RectF,
160 pub canvas_size: (u32, u32),
161 pub followers: Vec<(DecorationId, RectF)>,
163}
164impl Selectable<DecorationLayout> for DecorationElement {
165 fn bounds(&self, cfg: &DecorationLayout, _: &dyn MeasureText) -> Option<RectF> {
166 match self.id {
167 DecorationId::DataArea => return cfg.resolve_data_area(self.base_bounds).ok(),
168 DecorationId::DataAreaEdge(_) => return None,
169 _ => {}
170 }
171 let p = cfg.effective_offset(&self.id);
172 Some(self.base_bounds.translated(p[0], p[1]))
173 }
174 fn as_draggable(&self) -> Option<&dyn Draggable<DecorationLayout>> {
175 Some(self)
176 }
177 fn as_resizable(&self) -> Option<&dyn crate::resize::Resizable<DecorationLayout>> {
178 (self.id == DecorationId::DataArea).then_some(self)
179 }
180 fn element_id(&self) -> String {
181 self.id.element_id()
182 }
183}
184impl Draggable<DecorationLayout> for DecorationElement {
185 fn nudge_element(&self) -> DecorationElement {
186 self.clone()
187 }
188}
189impl crate::resize::Resizable<DecorationLayout> for DecorationElement {
190 fn resize_targets(&self, handle: crate::resize::ResizeHandle) -> (Option<Self>, Option<Self>) {
191 use crate::resize::ResizeHandle::*;
192 let edge = |side| {
193 let mut e = self.clone();
194 e.id = DecorationId::DataAreaEdge(side);
195 e
196 };
197 let x = match handle {
198 NW | W | SW => Some(edge(Side::Left)),
199 NE | E | SE => Some(edge(Side::Right)),
200 N | S => None,
201 };
202 let y = match handle {
203 NW | N | NE => Some(edge(Side::Top)),
204 SW | S | SE => Some(edge(Side::Bottom)),
205 E | W => None,
206 };
207 (x, y)
208 }
209 fn resize_by(
210 &self,
211 cfg: &mut DecorationLayout,
212 handle: crate::resize::ResizeHandle,
213 dx: f32,
214 dy: f32,
215 ) -> NudgeResult {
216 let reject = NudgeResult::Rejected(NudgeReject::OutOfBounds);
217 if self.id != DecorationId::DataArea || !dx.is_finite() || !dy.is_finite() {
218 return reject;
219 }
220 let (x, y) = self.resize_targets(handle);
221 let mut trial = cfg.clone();
222 for (element, dx, dy) in [(x, dx, 0.), (y, 0., dy)] {
223 if let Some(element) = element {
224 if trial.nudge(element, dx, dy) != NudgeResult::Moved {
225 return reject;
226 }
227 }
228 }
229 *cfg = trial;
230 NudgeResult::Moved
231 }
232}
233impl DragConfig for DecorationLayout {
234 type Element = DecorationElement;
235 fn nudge(&mut self, element: DecorationElement, mut dx: f32, mut dy: f32) -> NudgeResult {
236 let reject = NudgeResult::Rejected(NudgeReject::OutOfBounds);
237 if !dx.is_finite() || !dy.is_finite() {
238 return reject;
239 }
240 if matches!(
241 element.id,
242 DecorationId::DataArea | DecorationId::DataAreaEdge(_)
243 ) {
244 let Ok(mut r) = self.resolve_data_area(element.base_bounds) else {
245 return reject;
246 };
247 match element.id {
248 DecorationId::DataArea => {
249 r.x += dx;
250 r.y += dy;
251 }
252 DecorationId::DataAreaEdge(Side::Left) => {
253 r.x += dx;
254 r.width -= dx;
255 }
256 DecorationId::DataAreaEdge(Side::Right) => r.width += dx,
257 DecorationId::DataAreaEdge(Side::Top) => {
258 r.y += dy;
259 r.height -= dy;
260 }
261 DecorationId::DataAreaEdge(Side::Bottom) => r.height += dy,
262 _ => unreachable!(),
263 }
264 let mut trial = self.clone();
265 trial.data_area = Some(r);
266 if trial.resolve_data_area(element.base_bounds).is_err() {
267 return reject;
268 }
269 self.data_area = Some(r);
270 return NudgeResult::Moved;
271 }
272 if let DecorationId::Axis(side) = &element.id {
273 match side {
274 Side::Top | Side::Bottom => dx = 0.,
275 _ => dy = 0.,
276 }
277 }
278 let inside = |rect: RectF, offset: [f32; 2]| {
279 let r = rect.translated(offset[0] + dx, offset[1] + dy);
280 r.x >= 0.
281 && r.y >= 0.
282 && r.x + r.width <= element.canvas_size.0 as f32
283 && r.y + r.height <= element.canvas_size.1 as f32
284 };
285 if !inside(element.base_bounds, self.effective_offset(&element.id))
286 || element
287 .followers
288 .iter()
289 .any(|(id, rect)| !inside(*rect, self.effective_offset(id)))
290 {
291 return reject;
292 }
293 let old = self.offset(&element.id);
294 let next = [old[0] + dx, old[1] + dy];
295 self.reset(&element.id);
296 if next != [0., 0.] {
297 self.offsets.push(DecorationOffset {
298 element: element.id,
299 offset: next,
300 });
301 }
302 NudgeResult::Moved
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 #[test]
310 fn area_handles_move_only_their_edges_and_reject_atomically() {
311 use crate::resize::{Resizable, ResizeHandle::*};
312 let e = DecorationElement {
313 id: DecorationId::DataArea,
314 base_bounds: RectF {
315 x: 20.,
316 y: 20.,
317 width: 240.,
318 height: 160.,
319 },
320 canvas_size: (300, 200),
321 followers: vec![],
322 };
323 let original = RectF {
324 x: 50.,
325 y: 50.,
326 width: 150.,
327 height: 90.,
328 };
329 for (handle, left, right, top, bottom) in [
330 (NW, true, false, true, false),
331 (N, false, false, true, false),
332 (NE, false, true, true, false),
333 (E, false, true, false, false),
334 (SE, false, true, false, true),
335 (S, false, false, false, true),
336 (SW, true, false, false, true),
337 (W, true, false, false, false),
338 ] {
339 let mut state = DecorationLayout {
340 data_area: Some(original),
341 ..Default::default()
342 };
343 assert_eq!(e.resize_by(&mut state, handle, 7., 9.), NudgeResult::Moved);
344 let r = state.data_area.unwrap();
345 assert_eq!(r.x, original.x + if left { 7. } else { 0. });
346 assert_eq!(r.y, original.y + if top { 9. } else { 0. });
347 assert_eq!(
348 r.x + r.width,
349 original.x + original.width + if right { 7. } else { 0. }
350 );
351 assert_eq!(
352 r.y + r.height,
353 original.y + original.height + if bottom { 9. } else { 0. }
354 );
355 }
356 let mut state = DecorationLayout {
357 data_area: Some(original),
358 ..Default::default()
359 };
360 let before = state.clone();
361 for (dx, dy) in [
362 (7., -1000.),
363 (f32::NAN, 0.),
364 (0., f32::INFINITY),
365 (100., 0.),
366 ] {
367 assert!(matches!(
368 e.resize_by(&mut state, NW, dx, dy),
369 NudgeResult::Rejected(_)
370 ));
371 assert_eq!(state, before);
372 }
373 state.reset(&DecorationId::DataArea);
374 assert_eq!(
375 state.resolve_data_area(e.base_bounds).unwrap(),
376 e.base_bounds
377 );
378 }
379 fn element(id: DecorationId) -> DecorationElement {
380 DecorationElement {
381 id,
382 base_bounds: RectF {
383 x: 20.,
384 y: 20.,
385 width: 100.,
386 height: 20.,
387 },
388 canvas_size: (300, 200),
389 followers: vec![],
390 }
391 }
392 #[test]
393 fn cartesian_hit_map_constructors_keep_type_inference_without_annotations() {
394 let empty = crate::HitMap::new();
395 let default = crate::HitMap::default();
396 assert!(empty.is_empty() && default.is_empty());
397 }
398 #[test]
399 fn drag_rejects_nonfinite_outside_and_axis_follower_overflow_atomically() {
400 let mut state = DecorationLayout::default();
401 let mut axis = element(DecorationId::Axis(Side::Left));
402 axis.followers.push((
403 DecorationId::AxisLabel(Side::Left),
404 RectF {
405 x: 1.,
406 y: 20.,
407 width: 20.,
408 height: 30.,
409 },
410 ));
411 for delta in [[f32::NAN, 0.], [0., f32::INFINITY], [-2., 0.], [1000., 0.]] {
412 assert!(matches!(
413 axis.drag_by(&mut state, delta[0], delta[1]),
414 NudgeResult::Rejected(_)
415 ));
416 assert_eq!(state, DecorationLayout::default());
417 }
418 assert_eq!(axis.drag_by(&mut state, 5., 80.), NudgeResult::Moved);
419 assert_eq!(state.offset(&axis.id), [5., 0.]);
420 assert_eq!(
421 state.effective_offset(&DecorationId::AxisLabel(Side::Left)),
422 [5., 0.]
423 );
424 }
425 #[test]
426 fn duplicate_nonfinite_and_parallel_axis_offsets_are_rejected() {
427 let mut state = DecorationLayout {
428 data_area: None,
429 offsets: vec![DecorationOffset {
430 element: DecorationId::Legend,
431 offset: [f32::NAN, 0.],
432 }],
433 selected: None,
434 };
435 assert!(state.validate().is_err());
436 state.offsets[0].offset = [1., 2.];
437 state.offsets.push(state.offsets[0].clone());
438 assert!(state.validate().is_err());
439 state.offsets.pop();
440 state.offsets[0].element = DecorationId::Axis(Side::Left);
441 assert!(state.validate().is_err());
442 }
443 #[cfg(feature = "serde")]
444 #[test]
445 fn old_documents_default_positions_and_all_models_preserve_edits() {
446 for mut value in [
447 serde_json::to_value(crate::RadialChart::default()).unwrap(),
448 serde_json::to_value(crate::CategoricalChart::default()).unwrap(),
449 serde_json::to_value(crate::BoxPlotChart::default()).unwrap(),
450 ] {
451 value.as_object_mut().unwrap().remove("decorations");
452 assert!(value.get("decorations").is_none());
453 if value.get("slices").is_some() {
455 assert_eq!(
456 serde_json::from_value::<crate::RadialChart>(value)
457 .unwrap()
458 .decorations,
459 DecorationLayout::default()
460 );
461 } else if value.get("mode").is_some() {
462 assert_eq!(
463 serde_json::from_value::<crate::CategoricalChart>(value)
464 .unwrap()
465 .decorations,
466 DecorationLayout::default()
467 );
468 } else {
469 assert_eq!(
470 serde_json::from_value::<crate::BoxPlotChart>(value)
471 .unwrap()
472 .decorations,
473 DecorationLayout::default()
474 );
475 }
476 }
477 let mut state = DecorationLayout::default();
478 element(DecorationId::ChartTitle).drag_by(&mut state, 4., 6.);
479 state.selected = Some(DecorationId::ChartTitle);
480 let mut radial = crate::RadialChart::default();
481 radial.decorations = state.clone();
482 let mut categorical = crate::CategoricalChart::default();
483 categorical.decorations = state.clone();
484 let mut boxplot = crate::BoxPlotChart::default();
485 boxplot.decorations = state;
486 assert_eq!(
487 serde_json::from_str::<crate::RadialChart>(&serde_json::to_string(&radial).unwrap())
488 .unwrap(),
489 radial
490 );
491 assert_eq!(
492 serde_json::from_str::<crate::CategoricalChart>(
493 &serde_json::to_string(&categorical).unwrap()
494 )
495 .unwrap(),
496 categorical
497 );
498 assert_eq!(
499 serde_json::from_str::<crate::BoxPlotChart>(&serde_json::to_string(&boxplot).unwrap())
500 .unwrap(),
501 boxplot
502 );
503 }
504}