1use crate::config::Config;
22use crate::layout::{Element, NudgeResult};
23use crate::select::{
24 AxisElement, AxisLabelElement, AxisTitleElement, ChartTitleElement, ColorBarAxisElement,
25 ColorBarElement, ColorBarLabelElement, ColorBarTitleElement, DataAreaElement, LegendElement,
26 Selectable,
27};
28
29pub trait DragConfig {
32 type Element;
33 fn nudge(&mut self, element: Self::Element, dx: f32, dy: f32) -> NudgeResult;
34}
35impl DragConfig for Config {
36 type Element = Element;
37 fn nudge(&mut self, element: Element, dx: f32, dy: f32) -> NudgeResult {
38 Config::nudge(self, element, dx, dy)
39 }
40}
41
42pub trait Draggable<C: DragConfig = Config>: Selectable<C> {
44 fn nudge_element(&self) -> C::Element;
48
49 fn drag_by(&self, cfg: &mut C, dx: f32, dy: f32) -> NudgeResult {
57 cfg.nudge(self.nudge_element(), dx, dy)
58 }
59}
60
61impl Draggable for ChartTitleElement {
62 fn nudge_element(&self) -> Element {
63 Element::ChartTitle
64 }
65}
66
67impl Draggable for AxisTitleElement {
68 fn nudge_element(&self) -> Element {
69 Element::AxisTitle(self.side.clone())
70 }
71}
72
73impl Draggable for AxisLabelElement {
74 fn nudge_element(&self) -> Element {
75 Element::AxisLabel(self.side.clone())
76 }
77}
78
79impl Draggable for AxisElement {
80 fn nudge_element(&self) -> Element {
81 Element::Axis(self.side.clone())
82 }
83}
84
85impl Draggable for LegendElement {
86 fn nudge_element(&self) -> Element {
87 Element::Legend
88 }
89}
90
91impl Draggable for DataAreaElement {
92 fn nudge_element(&self) -> Element {
93 Element::DataArea
94 }
95}
96
97impl Draggable for ColorBarElement {
98 fn nudge_element(&self) -> Element {
99 Element::ColorBar
100 }
101}
102
103impl Draggable for ColorBarAxisElement {
104 fn nudge_element(&self) -> Element {
105 Element::ColorBarAxis
106 }
107}
108
109impl Draggable for ColorBarLabelElement {
110 fn nudge_element(&self) -> Element {
111 Element::ColorBarLabel
112 }
113}
114
115impl Draggable for ColorBarTitleElement {
116 fn nudge_element(&self) -> Element {
117 Element::ColorBarTitle
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use crate::default::default_config;
125 use crate::layout::{ChartArea, NudgeReject, Rect, Side};
126 use crate::text::rich_segments_from_text;
127
128 fn cfg_800x600() -> Config {
129 let mut cfg = default_config();
130 cfg.chart_area = ChartArea(Rect {
131 x: 0,
132 y: 0,
133 width: 800,
134 height: 600,
135 });
136 cfg.chart_title.text.segments = rich_segments_from_text("Title");
137 cfg
138 }
139
140 #[test]
141 fn drag_moves_chart_title_offset() {
142 let mut cfg = cfg_800x600();
143 let r = ChartTitleElement.drag_by(&mut cfg, 5.0, 3.0);
144 assert_eq!(r, NudgeResult::Moved);
145 assert_eq!(cfg.chart_title.offset_x, 5.0);
146 assert_eq!(cfg.chart_title.offset_y, 3.0);
147 }
148
149 #[test]
150 fn drag_matches_direct_nudge_path() {
151 let mut via_drag = cfg_800x600();
152 let mut via_nudge = cfg_800x600();
153 let el = AxisTitleElement { side: Side::Left };
154 el.drag_by(&mut via_drag, 4.0, -2.0);
155 via_nudge.nudge(Element::AxisTitle(Side::Left), 4.0, -2.0);
156 assert_eq!(via_drag, via_nudge);
157 }
158
159 #[test]
160 fn rejected_drag_leaves_config_unchanged() {
161 let mut cfg = cfg_800x600();
162 let before = cfg.clone();
163 let r = ChartTitleElement.drag_by(&mut cfg, -1e6, 0.0);
164 assert_eq!(r, NudgeResult::Rejected(NudgeReject::OutOfBounds));
165 assert_eq!(cfg, before);
166 }
167
168 #[test]
169 fn left_axis_title_drag_respects_rotated_frame() {
170 let mut cfg = cfg_800x600();
173 cfg.left_y.title_option.text.segments = rich_segments_from_text("Y");
174 let el = AxisTitleElement { side: Side::Left };
175 let r = el.drag_by(&mut cfg, 3.0, 7.0);
176 assert_eq!(r, NudgeResult::Moved);
177 assert_eq!(cfg.left_y.title_option.offset_x, -7.0);
178 assert_eq!(cfg.left_y.title_option.offset_y, 3.0);
179 }
180
181 #[test]
185 fn y_axis_drag_detaches_horizontally_only() {
186 let mut cfg = cfg_800x600();
187 let da_before = cfg.data_area().unwrap();
188
189 let el = AxisElement { side: Side::Left };
190 let r = el.drag_by(&mut cfg, 12.0, -40.0);
191 assert_eq!(r, NudgeResult::Moved);
192 assert_eq!(cfg.left_y.line_offset, 12.0);
193 assert_eq!(cfg.data_area().unwrap(), da_before);
194 }
195
196 #[test]
197 fn x_axis_drag_detaches_vertically_only() {
198 let mut cfg = cfg_800x600();
199 let da_before = cfg.data_area().unwrap();
200 let el = AxisElement { side: Side::Bottom };
201 let r = el.drag_by(&mut cfg, -40.0, -6.0);
202 assert_eq!(r, NudgeResult::Moved);
203 assert_eq!(cfg.bottom_x.line_offset, -6.0);
204 assert_eq!(cfg.data_area().unwrap(), da_before);
205 }
206
207 #[test]
208 fn axis_label_drag_moves_label_offsets() {
209 let mut cfg = cfg_800x600();
210 let el = AxisLabelElement { side: Side::Bottom };
211 let r = el.drag_by(&mut cfg, 2.0, 5.0);
212 assert_eq!(r, NudgeResult::Moved);
213 assert_eq!(cfg.bottom_x.label_style.label_offset_x, 2.0);
214 assert_eq!(cfg.bottom_x.label_style.label_offset_y, 5.0);
215 }
216
217 #[test]
219 fn as_draggable_exposes_drag_through_hitmap() {
220 use crate::select::{HitMap, Selectable};
221 use crate::text::{MeasureText, RichText, TextExtents};
222
223 struct FixedMeasure;
224 impl MeasureText for FixedMeasure {
225 fn measure_rich(&self, rt: &RichText) -> TextExtents {
226 TextExtents {
227 width: rt.segments.len() as f32 * 8.0,
228 ascent: 10.0,
229 descent: 3.0,
230 }
231 }
232 }
233
234 let mut cfg = cfg_800x600();
235 let map = HitMap::standard_chart();
236
237 let tb = ChartTitleElement.bounds(&cfg, &FixedMeasure).unwrap();
239 let id = map
240 .hit_test(&cfg, &FixedMeasure, tb.x + 1.0, tb.y + 1.0)
241 .unwrap();
242 let drag = map
243 .get(id)
244 .unwrap()
245 .as_draggable()
246 .expect("title is draggable");
247 assert_eq!(drag.drag_by(&mut cfg, 4.0, 2.0), NudgeResult::Moved);
248 assert_eq!(cfg.chart_title.offset_x, 4.0);
249
250 let db = crate::select::DataAreaElement
252 .bounds(&cfg, &FixedMeasure)
253 .unwrap();
254 let id2 = map
255 .hit_test(
256 &cfg,
257 &FixedMeasure,
258 db.x + db.width * 0.5,
259 db.y + db.height * 0.5,
260 )
261 .unwrap();
262 assert!(map.get(id2).unwrap().as_draggable().is_some());
263 }
264
265 #[test]
269 fn data_area_drag_translates_without_resize() {
270 let mut cfg = cfg_800x600();
271 let da_before = cfg.data_area().unwrap();
272
273 let r = DataAreaElement.drag_by(&mut cfg, 5.0, 3.0);
274 assert_eq!(r, NudgeResult::Moved);
275 let da_after = cfg.data_area().unwrap();
276 assert_eq!(da_after.x, da_before.x + 5);
277 assert_eq!(da_after.y, da_before.y + 3);
278 assert_eq!(da_after.width, da_before.width);
279 assert_eq!(da_after.height, da_before.height);
280 }
281
282 #[test]
285 fn data_area_drag_clamps_per_axis() {
286 let mut cfg = cfg_800x600();
287 let da_before = cfg.data_area().unwrap();
288 let right_margin = cfg.right_y.out_margin; let r = DataAreaElement.drag_by(&mut cfg, right_margin + 50.0, 3.0);
291 assert_eq!(r, NudgeResult::Moved);
292 let da_after = cfg.data_area().unwrap();
293 assert_eq!(
294 da_after.x, da_before.x,
295 "blocked horizontal component must not apply"
296 );
297 assert_eq!(da_after.y, da_before.y + 3);
298 }
299
300 #[test]
301 fn data_area_drag_rejected_when_fully_blocked() {
302 let mut cfg = cfg_800x600();
303 let before = cfg.clone();
304 let r = DataAreaElement.drag_by(&mut cfg, 1e6, -1e6);
305 assert_eq!(
306 r,
307 NudgeResult::Rejected(crate::layout::NudgeReject::OutOfBounds)
308 );
309 assert_eq!(cfg, before);
310 }
311
312 #[test]
316 fn dragging_the_colorbar_moves_its_offsets_and_not_the_layout() {
317 use crate::layout::Side;
318 use crate::select::ColorBarElement;
319
320 let mut cfg = cfg_800x600();
321 cfg.colorbar = Some(crate::default::default_colorbar_options());
322 let margins_before = cfg.margins();
323 let area_before = cfg.data_area().expect("data area");
324
325 assert_eq!(
326 ColorBarElement.drag_by(&mut cfg, 7.0, -3.0),
327 NudgeResult::Moved
328 );
329 let bar = cfg.colorbar.as_ref().expect("colourbar");
330 assert_eq!((bar.offset_x, bar.offset_y), (7.0, -3.0));
331 assert_eq!(cfg.margins(), margins_before);
332 assert_eq!(cfg.data_area().expect("data area"), area_before);
333
334 let da = cfg.data_area().expect("data area");
336 let anchored = {
337 let mut probe = cfg.clone();
338 let bar = probe.colorbar.as_mut().expect("colourbar");
339 bar.offset_x = 0.0;
340 bar.offset_y = 0.0;
341 crate::layout::colorbar_rect(
342 &probe.chart_area,
343 &da,
344 probe.chart_title.top_margin,
345 probe.colorbar.as_ref().expect("colourbar"),
346 )
347 };
348 ColorBarElement.drag_by(&mut cfg, 1.0, 1.0);
349 let bar = cfg.colorbar.as_ref().expect("colourbar");
350 assert_eq!((bar.offset_x, bar.offset_y), (8.0, -2.0));
351 let moved =
352 crate::layout::colorbar_rect(&cfg.chart_area, &da, cfg.chart_title.top_margin, bar);
353 assert_eq!(moved.x, anchored.x + 8.0);
354 assert_eq!(moved.y, anchored.y - 2.0);
355
356 let before = cfg.clone();
358 assert_eq!(
359 ColorBarElement.drag_by(&mut cfg, 10_000.0, 0.0),
360 NudgeResult::Rejected(NudgeReject::OutOfBounds)
361 );
362 assert_eq!(cfg, before);
363
364 let mut bare = cfg_800x600();
366 bare.colorbar = None;
367 assert_eq!(
368 ColorBarElement.drag_by(&mut bare, 1.0, 1.0),
369 NudgeResult::Rejected(NudgeReject::OutOfBounds)
370 );
371 let _ = Side::Right;
372 }
373
374 #[test]
375 fn colorbar_detail_drags_update_only_the_z_axis_fields() {
376 let mut cfg = cfg_800x600();
377 let mut bar = crate::default::default_colorbar_options();
378 bar.side = Side::Right;
379 bar.axis.title_option.visible = true;
380 bar.axis.title_option.text =
381 crate::text::RichText::plain("z", crate::color::Color::BLACK, 12.0, "");
382 cfg.colorbar = Some(bar);
383 let area_before = cfg.data_area().expect("data area");
384 let strip_offsets_before = {
385 let bar = cfg.colorbar.as_ref().unwrap();
386 (bar.offset_x, bar.offset_y)
387 };
388
389 assert_eq!(
390 ColorBarAxisElement.drag_by(&mut cfg, 6.0, 99.0),
391 NudgeResult::Moved
392 );
393 assert_eq!(cfg.colorbar.as_ref().unwrap().axis.line_offset, 6.0);
394
395 assert_eq!(
396 ColorBarLabelElement.drag_by(&mut cfg, 4.0, -2.0),
397 NudgeResult::Moved
398 );
399 let labels = &cfg.colorbar.as_ref().unwrap().axis.label_style;
400 assert_eq!((labels.label_offset_x, labels.label_offset_y), (4.0, -2.0));
401
402 assert_eq!(
403 ColorBarTitleElement.drag_by(&mut cfg, 3.0, 7.0),
404 NudgeResult::Moved
405 );
406 let title = &cfg.colorbar.as_ref().unwrap().axis.title_option;
407 assert_eq!((title.offset_x, title.offset_y), (7.0, -3.0));
409
410 let bar = cfg.colorbar.as_ref().unwrap();
411 assert_eq!((bar.offset_x, bar.offset_y), strip_offsets_before);
412 assert_eq!(cfg.data_area().expect("data area"), area_before);
413 }
414}