1use crate::config::Config;
15use crate::drag::DragConfig;
16use crate::layout::{Element, NudgeReject, NudgeResult, RectF, Side};
17use crate::select::{ColorBarElement, DataAreaElement, SELECTION_PADDING, Selectable};
18use crate::text::MeasureText;
19
20pub const HANDLE_SIZE: f32 = 8.0;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum ResizeHandle {
26 NW,
27 N,
28 NE,
29 E,
30 SE,
31 S,
32 SW,
33 W,
34}
35
36pub const ALL_HANDLES: [ResizeHandle; 8] = [
37 ResizeHandle::NW,
38 ResizeHandle::N,
39 ResizeHandle::NE,
40 ResizeHandle::E,
41 ResizeHandle::SE,
42 ResizeHandle::S,
43 ResizeHandle::SW,
44 ResizeHandle::W,
45];
46
47pub fn handle_rects(rect: &RectF) -> [(ResizeHandle, RectF); 8] {
50 let (x0, y0) = (rect.x, rect.y);
51 let (x1, y1) = (rect.x + rect.width, rect.y + rect.height);
52 let (cx, cy) = (rect.x + rect.width * 0.5, rect.y + rect.height * 0.5);
53 let half = HANDLE_SIZE * 0.5;
54 let square = |x: f32, y: f32| RectF {
55 x: x - half,
56 y: y - half,
57 width: HANDLE_SIZE,
58 height: HANDLE_SIZE,
59 };
60 [
61 (ResizeHandle::NW, square(x0, y0)),
62 (ResizeHandle::N, square(cx, y0)),
63 (ResizeHandle::NE, square(x1, y0)),
64 (ResizeHandle::E, square(x1, cy)),
65 (ResizeHandle::SE, square(x1, y1)),
66 (ResizeHandle::S, square(cx, y1)),
67 (ResizeHandle::SW, square(x0, y1)),
68 (ResizeHandle::W, square(x0, cy)),
69 ]
70}
71
72pub trait Resizable<C: DragConfig = Config>: Selectable<C> {
75 fn resize_targets(&self, handle: ResizeHandle) -> (Option<C::Element>, Option<C::Element>);
79
80 fn resize_handles(
83 &self,
84 cfg: &C,
85 measure: &dyn MeasureText,
86 ) -> Option<[(ResizeHandle, RectF); 8]> {
87 let rect = self.bounds(cfg, measure)?.expanded(SELECTION_PADDING);
88 Some(handle_rects(&rect))
89 }
90
91 fn hit_resize_handle(
93 &self,
94 cfg: &C,
95 measure: &dyn MeasureText,
96 x: f32,
97 y: f32,
98 ) -> Option<ResizeHandle> {
99 self.resize_handles(cfg, measure)?
100 .iter()
101 .find(|(_, r)| r.contains(x, y))
102 .map(|(h, _)| *h)
103 }
104
105 fn resize_by(&self, cfg: &mut C, handle: ResizeHandle, dx: f32, dy: f32) -> NudgeResult {
109 let (h_target, v_target) = self.resize_targets(handle);
110 let mut moved = false;
111 let mut attempted = false;
112 if dx != 0.0
113 && let Some(e) = h_target
114 {
115 attempted = true;
116 moved |= cfg.nudge(e, dx, 0.0) == NudgeResult::Moved;
117 }
118 if dy != 0.0
119 && let Some(e) = v_target
120 {
121 attempted = true;
122 moved |= cfg.nudge(e, 0.0, dy) == NudgeResult::Moved;
123 }
124 if moved || !attempted {
125 NudgeResult::Moved
126 } else {
127 NudgeResult::Rejected(NudgeReject::OutOfBounds)
128 }
129 }
130}
131
132impl Resizable for ColorBarElement {
133 fn resize_targets(&self, handle: ResizeHandle) -> (Option<Element>, Option<Element>) {
138 use ResizeHandle::*;
139 let horizontal = match handle {
140 NW | W | SW => Some(Element::ColorBarEdge(Side::Left)),
141 NE | E | SE => Some(Element::ColorBarEdge(Side::Right)),
142 N | S => None,
143 };
144 let vertical = match handle {
145 NW | N | NE => Some(Element::ColorBarEdge(Side::Top)),
146 SW | S | SE => Some(Element::ColorBarEdge(Side::Bottom)),
147 E | W => None,
148 };
149 (horizontal, vertical)
150 }
151}
152
153impl Resizable for DataAreaElement {
154 fn resize_targets(&self, handle: ResizeHandle) -> (Option<Element>, Option<Element>) {
156 use ResizeHandle::*;
157 let horizontal = match handle {
158 NW | W | SW => Some(Element::DataAreaEdge(Side::Left)),
159 NE | E | SE => Some(Element::DataAreaEdge(Side::Right)),
160 N | S => None,
161 };
162 let vertical = match handle {
163 NW | N | NE => Some(Element::DataAreaEdge(Side::Top)),
164 SW | S | SE => Some(Element::DataAreaEdge(Side::Bottom)),
165 E | W => None,
166 };
167 (horizontal, vertical)
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use crate::default::default_config;
175 use crate::layout::{ChartArea, Rect};
176 use crate::text::{RichText, TextExtents};
177
178 struct FixedMeasure;
179 impl MeasureText for FixedMeasure {
180 fn measure_rich(&self, rt: &RichText) -> TextExtents {
181 TextExtents {
182 width: rt.segments.len() as f32 * 8.0,
183 ascent: 10.0,
184 descent: 3.0,
185 }
186 }
187 }
188
189 fn cfg_800x600() -> Config {
190 let mut cfg = default_config();
191 cfg.chart_area = ChartArea(Rect {
192 x: 0,
193 y: 0,
194 width: 800,
195 height: 600,
196 });
197 cfg
198 }
199
200 #[test]
201 fn handles_sit_on_selection_box_corners_and_midpoints() {
202 let cfg = cfg_800x600();
203 let b = DataAreaElement
204 .bounds(&cfg, &FixedMeasure)
205 .unwrap()
206 .expanded(SELECTION_PADDING);
207 let handles = DataAreaElement.resize_handles(&cfg, &FixedMeasure).unwrap();
208 assert_eq!(handles.len(), 8);
209
210 let (_, nw) = handles[0];
211 assert_eq!(nw.x + HANDLE_SIZE * 0.5, b.x);
212 assert_eq!(nw.y + HANDLE_SIZE * 0.5, b.y);
213 let (_, e) = handles[3];
214 assert_eq!(e.x + HANDLE_SIZE * 0.5, b.x + b.width);
215 assert_eq!(e.y + HANDLE_SIZE * 0.5, b.y + b.height * 0.5);
216 }
217
218 #[test]
219 fn hit_resize_handle_finds_corner() {
220 let cfg = cfg_800x600();
221 let handles = DataAreaElement.resize_handles(&cfg, &FixedMeasure).unwrap();
222 let (kind, rect) = handles[4]; let hit = DataAreaElement
224 .hit_resize_handle(
225 &cfg,
226 &FixedMeasure,
227 rect.x + rect.width * 0.5,
228 rect.y + rect.height * 0.5,
229 )
230 .unwrap();
231 assert_eq!(hit, kind);
232 assert!(
233 DataAreaElement
234 .hit_resize_handle(&cfg, &FixedMeasure, -100.0, -100.0)
235 .is_none()
236 );
237 }
238
239 #[test]
242 fn east_handle_resizes_via_right_axis() {
243 let mut cfg = cfg_800x600();
244 let da_before = cfg.data_area().unwrap();
245 let r = DataAreaElement.resize_by(&mut cfg, ResizeHandle::E, -10.0, 99.0);
246 assert_eq!(r, NudgeResult::Moved);
247 let da_after = cfg.data_area().unwrap();
248 assert_eq!(da_after.width, da_before.width - 10);
249 assert_eq!(da_after.height, da_before.height);
250 assert_eq!(da_after.x, da_before.x);
251 }
252
253 #[test]
255 fn nw_corner_resizes_left_and_top() {
256 let mut cfg = cfg_800x600();
257 let da_before = cfg.data_area().unwrap();
258 let r = DataAreaElement.resize_by(&mut cfg, ResizeHandle::NW, 6.0, 4.0);
259 assert_eq!(r, NudgeResult::Moved);
260 let da_after = cfg.data_area().unwrap();
261 assert_eq!(da_after.x, da_before.x + 6);
262 assert_eq!(da_after.y, da_before.y + 4);
263 assert_eq!(da_after.width, da_before.width - 6);
264 assert_eq!(da_after.height, da_before.height - 4);
265 }
266
267 #[test]
269 fn resize_rejected_when_margin_would_go_negative() {
270 let mut cfg = cfg_800x600();
271 let before = cfg.clone();
272 let r = DataAreaElement.resize_by(&mut cfg, ResizeHandle::E, 1e6, 0.0);
273 assert_eq!(r, NudgeResult::Rejected(NudgeReject::OutOfBounds));
274 assert_eq!(cfg, before);
275 }
276
277 fn cfg_with_colorbar(side: Side) -> Config {
280 let mut cfg = cfg_800x600();
281 let mut bar = crate::default::default_colorbar_options();
282 bar.side = side;
283 cfg.colorbar = Some(bar);
284 cfg
285 }
286
287 fn bar_of(cfg: &Config) -> &crate::config::ColorBarOptions {
288 cfg.colorbar.as_ref().expect("colourbar")
289 }
290
291 fn colorbar_bounds(cfg: &Config) -> RectF {
292 ColorBarElement
293 .bounds(cfg, &FixedMeasure)
294 .expect("strip bounds")
295 }
296
297 fn colorbar_short_edges(rect: RectF, side: &Side) -> (f32, f32) {
298 match side {
299 Side::Left => (rect.x + rect.width, rect.x),
300 Side::Right => (rect.x, rect.x + rect.width),
301 Side::Top => (rect.y + rect.height, rect.y),
302 Side::Bottom => (rect.y, rect.y + rect.height),
303 }
304 }
305
306 fn assert_near(actual: f32, expected: f32, context: &str) {
307 assert!(
308 (actual - expected).abs() < 1e-3,
309 "{context}: got {actual}, expected {expected}"
310 );
311 }
312
313 #[test]
317 fn colorbar_handles_drive_thickness_or_length_by_orientation() {
318 for (side, thickness_handle, length_handle) in [
319 (Side::Right, ResizeHandle::W, ResizeHandle::N),
320 (Side::Left, ResizeHandle::E, ResizeHandle::S),
321 (Side::Top, ResizeHandle::S, ResizeHandle::E),
322 (Side::Bottom, ResizeHandle::N, ResizeHandle::W),
323 ] {
324 let base = cfg_with_colorbar(side.clone());
325 let (t0, l0) = (bar_of(&base).thickness_px, bar_of(&base).length_frac);
326
327 let mut cfg = base.clone();
329 assert_eq!(
330 ColorBarElement.resize_by(&mut cfg, thickness_handle, -4.0, -4.0),
331 NudgeResult::Moved,
332 "{side:?} {thickness_handle:?}"
333 );
334 assert_ne!(bar_of(&cfg).thickness_px, t0, "{side:?} thickness");
335 assert_eq!(bar_of(&cfg).length_frac, l0, "{side:?} length untouched");
336
337 let mut cfg = base.clone();
339 assert_eq!(
340 ColorBarElement.resize_by(&mut cfg, length_handle, -4.0, -4.0),
341 NudgeResult::Moved,
342 "{side:?} {length_handle:?}"
343 );
344 assert_eq!(
345 bar_of(&cfg).thickness_px,
346 t0,
347 "{side:?} thickness untouched"
348 );
349 assert_ne!(bar_of(&cfg).length_frac, l0, "{side:?} length");
350 }
351 }
352
353 #[test]
356 fn dragging_a_colorbar_edge_outward_grows_it() {
357 let mut cfg = cfg_with_colorbar(Side::Right);
359 let before = colorbar_bounds(&cfg);
360 ColorBarElement.resize_by(&mut cfg, ResizeHandle::W, -6.0, 0.0);
361 let after = colorbar_bounds(&cfg);
362 assert!(
363 after.width > before.width,
364 "west drag left must widen: {} -> {}",
365 before.width,
366 after.width
367 );
368
369 let mut cfg = cfg_with_colorbar(Side::Right);
370 ColorBarElement.resize_by(&mut cfg, ResizeHandle::W, 6.0, 0.0);
371 assert!(
372 colorbar_bounds(&cfg).width < before.width,
373 "west drag right narrows"
374 );
375 }
376
377 #[test]
381 fn colorbar_thickness_handles_track_the_pointer_on_all_sides() {
382 for (side, outer_handle, outer_dx, outer_dy) in [
383 (Side::Left, ResizeHandle::W, -6.0, 0.0),
384 (Side::Right, ResizeHandle::E, 6.0, 0.0),
385 (Side::Top, ResizeHandle::N, 0.0, -6.0),
386 (Side::Bottom, ResizeHandle::S, 0.0, 6.0),
387 ] {
388 let pointer_delta = if outer_dx != 0.0 { outer_dx } else { outer_dy };
389
390 let mut cfg = cfg_with_colorbar(side.clone());
391 let before = colorbar_bounds(&cfg);
392 let (inner_before, outer_before) = colorbar_short_edges(before, &side);
393 assert_eq!(
394 ColorBarElement.resize_by(&mut cfg, outer_handle, outer_dx, outer_dy),
395 NudgeResult::Moved,
396 "{side:?} outer handle"
397 );
398 let after = colorbar_bounds(&cfg);
399 let (inner_after, outer_after) = colorbar_short_edges(after, &side);
400 assert_near(inner_after, inner_before, &format!("{side:?} inner edge"));
401 assert_near(
402 outer_after,
403 outer_before + pointer_delta,
404 &format!("{side:?} outer edge"),
405 );
406
407 let inner_handle = match side {
408 Side::Left => ResizeHandle::E,
409 Side::Right => ResizeHandle::W,
410 Side::Top => ResizeHandle::S,
411 Side::Bottom => ResizeHandle::N,
412 };
413 let mut cfg = cfg_with_colorbar(side.clone());
414 let before = colorbar_bounds(&cfg);
415 let (inner_before, outer_before) = colorbar_short_edges(before, &side);
416 assert_eq!(
417 ColorBarElement.resize_by(&mut cfg, inner_handle, -outer_dx, -outer_dy),
418 NudgeResult::Moved,
419 "{side:?} inner handle"
420 );
421 let after = colorbar_bounds(&cfg);
422 let (inner_after, outer_after) = colorbar_short_edges(after, &side);
423 assert_near(
424 inner_after,
425 inner_before - pointer_delta,
426 &format!("{side:?} inner edge"),
427 );
428 assert_near(outer_after, outer_before, &format!("{side:?} outer edge"));
429 }
430 }
431
432 #[test]
436 fn a_colorbar_end_handle_tracks_the_pointer_under_its_anchor() {
437 use crate::config::BarAlign;
438
439 for (align, factor) in [
440 (BarAlign::Center, 2.0f32),
441 (BarAlign::Start, 1.0),
442 (BarAlign::End, 1.0),
443 ] {
444 let mut cfg = cfg_with_colorbar(Side::Right);
445 cfg.colorbar.as_mut().expect("colourbar").align = align.clone();
446 let da = cfg.data_area().expect("data area");
447 let before = bar_of(&cfg).length_frac;
448
449 let drag = -10.0f32; ColorBarElement.resize_by(&mut cfg, ResizeHandle::N, 0.0, drag);
451 let grew = (bar_of(&cfg).length_frac - before) * da.height as f32;
452 assert!(
453 (grew - factor * -drag).abs() < 1e-3,
454 "{align:?}: grew {grew} px, expected {}",
455 factor * -drag
456 );
457 }
458 }
459
460 #[test]
463 fn colorbar_resize_stops_at_the_valid_range() {
464 let mut cfg = cfg_with_colorbar(Side::Right);
466 cfg.colorbar.as_mut().expect("colourbar").length_frac = 1.0;
467 let before = cfg.clone();
468 assert_eq!(
469 ColorBarElement.resize_by(&mut cfg, ResizeHandle::N, 0.0, -5.0),
470 NudgeResult::Rejected(NudgeReject::OutOfBounds)
471 );
472 assert_eq!(cfg, before);
473
474 let mut cfg = cfg_with_colorbar(Side::Right);
476 cfg.colorbar.as_mut().expect("colourbar").thickness_px = 2.0;
477 let before = cfg.clone();
478 assert_eq!(
479 ColorBarElement.resize_by(&mut cfg, ResizeHandle::W, 50.0, 0.0),
480 NudgeResult::Rejected(NudgeReject::OutOfBounds)
481 );
482 assert_eq!(cfg, before);
483
484 let mut cfg = cfg_with_colorbar(Side::Right);
487 let before = cfg.clone();
488 assert_eq!(
489 ColorBarElement.resize_by(&mut cfg, ResizeHandle::E, 1e6, 0.0),
490 NudgeResult::Rejected(NudgeReject::OutOfBounds)
491 );
492 assert_eq!(cfg, before);
493
494 let mut cfg = cfg_with_colorbar(Side::Right);
496 for _ in 0..40 {
497 ColorBarElement.resize_by(&mut cfg, ResizeHandle::NW, -3.0, -3.0);
498 assert!(cfg.validate().is_ok(), "resize left an invalid config");
499 }
500 }
501
502 #[test]
504 fn a_colorbar_corner_handle_resizes_both_dimensions() {
505 let mut cfg = cfg_with_colorbar(Side::Right);
506 let (t0, l0) = (bar_of(&cfg).thickness_px, bar_of(&cfg).length_frac);
507 assert_eq!(
508 ColorBarElement.resize_by(&mut cfg, ResizeHandle::NW, -5.0, -5.0),
509 NudgeResult::Moved
510 );
511 assert!(bar_of(&cfg).thickness_px > t0);
512 assert!(bar_of(&cfg).length_frac > l0);
513 }
514
515 #[test]
518 fn a_colorbar_outer_corner_tracks_both_pointer_components() {
519 let mut cfg = cfg_with_colorbar(Side::Right);
520 let before = colorbar_bounds(&cfg);
521 assert_eq!(
522 ColorBarElement.resize_by(&mut cfg, ResizeHandle::NE, 5.0, -7.0),
523 NudgeResult::Moved
524 );
525 let after = colorbar_bounds(&cfg);
526 assert_near(after.x, before.x, "opposite thickness edge");
527 assert_near(
528 after.x + after.width,
529 before.x + before.width + 5.0,
530 "outer thickness edge",
531 );
532 assert_near(after.y, before.y - 7.0, "north length edge");
533 }
534}