1use super::YUnit;
2use super::composition::PanelId;
3use crate::config::theme::{ThemeAccent, ThemeColorShade};
4use exchange::UnixMs;
5use serde::{Deserialize, Serialize};
6
7pub const MIN_DRAWING_STROKE_WIDTH: f32 = 0.5;
8pub const MAX_DRAWING_STROKE_WIDTH: f32 = 4.0;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub struct DrawingId(pub u64);
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
14pub enum DrawingTool {
15 #[default]
16 Cursor,
17 Trendline,
18 HorizontalLine,
19 VerticalLine,
20 Box,
21 Measure,
22}
23
24impl DrawingTool {
25 pub fn allows_panning(self) -> bool {
26 matches!(self, Self::Cursor)
27 }
28
29 pub const fn supports_magnet(self) -> bool {
30 matches!(
31 self,
32 Self::Trendline | Self::Box | Self::HorizontalLine | Self::VerticalLine
33 )
34 }
35
36 pub const fn all() -> &'static [Self] {
37 &[
38 Self::Cursor,
39 Self::Trendline,
40 Self::HorizontalLine,
41 Self::VerticalLine,
42 Self::Box,
43 Self::Measure,
44 ]
45 }
46
47 pub const fn label(self) -> &'static str {
48 match self {
49 Self::Cursor => "Crosshair",
50 Self::Trendline => "Trendline",
51 Self::Box => "Box",
52 Self::HorizontalLine => "Horizontal line",
53 Self::VerticalLine => "Vertical line",
54 Self::Measure => "Measure",
55 }
56 }
57
58 pub fn default_style(self) -> DrawingStyle {
59 let mut style = DrawingStyle::default();
60
61 match self {
62 Self::Cursor | Self::Measure => {}
63 Self::Trendline => {
64 style.stroke_color = DrawingColor::accent(ThemeAccent::One, 1.0);
65 style.stroke_width = 1.2;
66 }
67 Self::Box => {
68 style.stroke_color = DrawingColor::accent(ThemeAccent::One, 1.0);
69 style.stroke_width = 1.2;
70 style.fill_color = Some(DrawingColor::accent(ThemeAccent::One, 0.16));
71 }
72 Self::HorizontalLine => {
73 style.stroke_color = DrawingColor::accent(ThemeAccent::One, 1.0);
74 style.stroke_width = 1.0;
75 }
76 Self::VerticalLine => {
77 style.stroke_color = DrawingColor::accent(ThemeAccent::One, 1.0);
78 style.stroke_width = 1.0;
79 }
80 }
81
82 style
83 }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
87pub struct DrawingColor {
88 pub accent: ThemeAccent,
89 #[serde(default)]
90 pub shade: ThemeColorShade,
91 pub alpha: f32,
92}
93
94impl DrawingColor {
95 pub const fn accent(accent: ThemeAccent, alpha: f32) -> Self {
96 Self::themed(accent, ThemeColorShade::Base, alpha)
97 }
98
99 pub const fn themed(accent: ThemeAccent, shade: ThemeColorShade, alpha: f32) -> Self {
100 Self {
101 accent,
102 shade,
103 alpha,
104 }
105 }
106
107 pub fn with_accent(self, accent: ThemeAccent, alpha: f32) -> Self {
108 Self {
109 accent,
110 alpha,
111 ..self
112 }
113 }
114
115 pub fn scale_alpha(self, factor: f32) -> Self {
116 Self {
117 alpha: self.alpha * factor,
118 ..self
119 }
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq)]
124pub enum DrawingX {
125 Time(UnixMs),
126 Tick(f64),
127}
128
129impl Default for DrawingX {
130 fn default() -> Self {
131 Self::Time(UnixMs::new(0))
132 }
133}
134
135impl Serialize for DrawingX {
136 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
137 match self {
138 Self::Time(time) => time.serialize(serializer),
139 Self::Tick(_) => Err(serde::ser::Error::custom("tick drawings are session-only")),
140 }
141 }
142}
143
144impl<'de> Deserialize<'de> for DrawingX {
145 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
146 UnixMs::deserialize(deserializer).map(Self::Time)
147 }
148}
149
150impl PartialOrd for DrawingX {
151 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
152 match (*self, *other) {
153 (Self::Time(a), Self::Time(b)) => a.partial_cmp(&b),
154 (Self::Tick(a), Self::Tick(b)) if a.is_finite() && b.is_finite() => a.partial_cmp(&b),
155 _ => None,
156 }
157 }
158}
159
160impl DrawingX {
161 pub fn is_time(self) -> bool {
162 matches!(self, Self::Time(_))
163 }
164
165 pub fn is_valid(self) -> bool {
166 match self {
167 Self::Time(_) => true,
168 Self::Tick(position) => position.is_finite(),
169 }
170 }
171
172 pub fn compatible(self, other: Self) -> bool {
173 self.partial_cmp(&other).is_some()
174 }
175
176 pub fn min(self, other: Self) -> Self {
177 if self <= other { self } else { other }
178 }
179
180 pub fn max(self, other: Self) -> Self {
181 if self >= other { self } else { other }
182 }
183
184 pub fn tick_bar_position(self, trades_per_bar: u64) -> Option<f64> {
185 match self {
186 Self::Tick(position) if position.is_finite() && trades_per_bar > 0 => {
187 Some(position / trades_per_bar as f64)
188 }
189 _ => None,
190 }
191 }
192
193 fn translated(self, origin: Self, current: Self) -> Option<Self> {
194 match (self, origin, current) {
195 (Self::Time(time), Self::Time(origin), Self::Time(current)) => {
196 Some(Self::Time(if current >= origin {
197 time.saturating_add(current.saturating_diff(origin))
198 } else {
199 time.saturating_sub(origin.saturating_diff(current))
200 }))
201 }
202 (Self::Tick(position), Self::Tick(origin), Self::Tick(current))
203 if position.is_finite() && origin.is_finite() && current.is_finite() =>
204 {
205 let translated = position + (current - origin);
206 translated.is_finite().then_some(Self::Tick(translated))
207 }
208 _ => None,
209 }
210 }
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
214pub struct DrawingAnchor {
215 pub panel_id: PanelId,
216 #[serde(rename = "time")]
217 pub x: DrawingX,
218 pub y_unit: YUnit,
219}
220
221#[derive(Debug, Clone, PartialEq)]
222pub enum KlineWidgetDrawingEvent {
223 MagnetModifierChanged(bool),
224 Selected(Option<DrawingId>),
225 MarqueeSelected(Vec<DrawingId>),
226 AnchorPressed(DrawingAnchor),
227 AnchorMoved(DrawingAnchor),
228 DragStarted {
229 id: DrawingId,
230 target: DrawingDragTarget,
231 operation: DrawingDragOperation,
232 anchor: DrawingAnchor,
233 },
234 DragMoved {
235 id: DrawingId,
236 target: DrawingDragTarget,
237 anchor: DrawingAnchor,
238 },
239 DragFinished {
240 id: DrawingId,
241 },
242 MeasureClick(DrawingAnchor),
243 MeasureMove(DrawingAnchor),
244 MeasureCanceled,
245 DraftCanceled,
246}
247
248#[derive(Debug, Clone, Copy, PartialEq)]
249pub struct MeasureState {
250 pub panel_id: PanelId,
251 pub start: DrawingAnchor,
252 pub end: DrawingAnchor,
253}
254
255#[derive(Debug, Clone, Copy, PartialEq)]
256pub enum DrawingMeasure {
257 Pending(MeasureState),
258 Committed(MeasureState),
259}
260
261impl DrawingMeasure {
262 pub fn state(self) -> MeasureState {
263 match self {
264 Self::Pending(state) | Self::Committed(state) => state,
265 }
266 }
267}
268
269#[derive(Debug, Clone, Copy)]
270pub struct DrawingSnapshot<'a> {
271 pub active_tool: DrawingTool,
272 pub magnet_enabled: bool,
273 pub entities: &'a [DrawingEntity],
274 pub selected_drawings: &'a [DrawingId],
275 pub drawing_draft: Option<&'a DrawingDraft>,
276 pub measure: Option<DrawingMeasure>,
277}
278
279impl<'a> DrawingSnapshot<'a> {
280 pub const fn new(
281 active_tool: DrawingTool,
282 magnet_enabled: bool,
283 entities: &'a [DrawingEntity],
284 selected_drawings: &'a [DrawingId],
285 drawing_draft: Option<&'a DrawingDraft>,
286 measure: Option<DrawingMeasure>,
287 ) -> Self {
288 Self {
289 active_tool,
290 magnet_enabled,
291 entities,
292 selected_drawings,
293 drawing_draft,
294 measure,
295 }
296 }
297
298 pub fn allows_panning(&self) -> bool {
299 self.active_tool.allows_panning()
300 }
301
302 pub fn has_state(&self) -> bool {
303 !self.entities.is_empty()
304 || !self.selected_drawings.is_empty()
305 || self.drawing_draft.is_some()
306 || self.measure.is_some()
307 }
308
309 pub fn draft_panel_id(&self) -> Option<PanelId> {
310 self.drawing_draft.map(DrawingDraft::panel_id)
311 }
312
313 pub fn selected_visible_drawings(&self) -> impl Iterator<Item = &'a DrawingEntity> {
314 self.entities
315 .iter()
316 .filter(|drawing| drawing.visible && self.selected_drawings.contains(&drawing.id))
317 }
318
319 fn selected_visible_drawing(&self) -> Option<&'a DrawingEntity> {
320 if self.selected_drawings.len() == 1 {
321 self.selected_visible_drawings().next()
322 } else {
323 None
324 }
325 }
326
327 pub fn active_axis_labeled_object(&self) -> Option<DrawingObject> {
328 if let Some(draft) = self.drawing_draft {
329 return Some(draft.preview_object());
330 }
331
332 self.selected_visible_drawing()
333 .map(|drawing| drawing.object.clone())
334 }
335}
336
337#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
338pub struct DrawingStyle {
339 pub stroke_color: DrawingColor,
340 pub stroke_width: f32,
341 pub fill_color: Option<DrawingColor>,
342}
343
344impl Default for DrawingStyle {
345 fn default() -> Self {
346 Self {
347 stroke_color: DrawingColor::accent(ThemeAccent::One, 1.0),
348 stroke_width: 1.2,
349 fill_color: None,
350 }
351 }
352}
353
354impl DrawingStyle {
355 pub fn normalized_stroke_width(self) -> f32 {
356 Self::normalize_stroke_width(self.stroke_width)
357 }
358
359 pub fn set_stroke_width(&mut self, width: f32) -> bool {
360 let width = Self::normalize_stroke_width(width);
361
362 if self.stroke_width == width {
363 return false;
364 }
365
366 self.stroke_width = width;
367 true
368 }
369
370 fn normalize_stroke_width(width: f32) -> f32 {
371 if width.is_finite() {
372 width.clamp(MIN_DRAWING_STROKE_WIDTH, MAX_DRAWING_STROKE_WIDTH)
373 } else {
374 Self::default().stroke_width
375 }
376 }
377}
378
379#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
380#[serde(default)]
381pub struct DrawingStyleDefaults {
382 pub trendline: DrawingStyle,
383 pub box_drawing: DrawingStyle,
384 pub horizontal_line: DrawingStyle,
385 pub vertical_line: DrawingStyle,
386}
387
388impl Default for DrawingStyleDefaults {
389 fn default() -> Self {
390 Self {
391 trendline: DrawingTool::Trendline.default_style(),
392 box_drawing: DrawingTool::Box.default_style(),
393 horizontal_line: DrawingTool::HorizontalLine.default_style(),
394 vertical_line: DrawingTool::VerticalLine.default_style(),
395 }
396 }
397}
398
399impl DrawingStyleDefaults {
400 pub fn style_for(self, tool: DrawingTool) -> DrawingStyle {
401 match tool {
402 DrawingTool::Trendline => self.trendline,
403 DrawingTool::Box => self.box_drawing,
404 DrawingTool::HorizontalLine => self.horizontal_line,
405 DrawingTool::VerticalLine => self.vertical_line,
406 DrawingTool::Cursor | DrawingTool::Measure => tool.default_style(),
407 }
408 }
409
410 pub fn set_style(&mut self, tool: DrawingTool, style: DrawingStyle) -> bool {
411 let stored_style = match tool {
412 DrawingTool::Trendline => &mut self.trendline,
413 DrawingTool::Box => &mut self.box_drawing,
414 DrawingTool::HorizontalLine => &mut self.horizontal_line,
415 DrawingTool::VerticalLine => &mut self.vertical_line,
416 DrawingTool::Cursor | DrawingTool::Measure => return false,
417 };
418
419 if *stored_style == style {
420 return false;
421 }
422
423 *stored_style = style;
424 true
425 }
426}
427
428#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
429#[serde(default)]
430pub struct DrawingSettings {
431 pub style_defaults: DrawingStyleDefaults,
432 pub horizontal_line_ray: bool,
433 pub box_extend_right: bool,
434}
435
436#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
437pub enum DrawingObject {
438 Trendline {
439 start: DrawingAnchor,
440 end: DrawingAnchor,
441 },
442 Box {
443 start: DrawingAnchor,
444 end: DrawingAnchor,
445 #[serde(default)]
446 extend_right: bool,
447 },
448 HorizontalLine {
449 panel_id: PanelId,
450 y_unit: YUnit,
451 #[serde(default)]
452 #[serde(rename = "time")]
453 x: DrawingX,
454 #[serde(default)]
455 ray: bool,
456 },
457 VerticalLine {
458 #[serde(rename = "time")]
459 x: DrawingX,
460 },
461}
462
463#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
464pub struct DrawingEntity {
465 pub id: DrawingId,
466 pub object: DrawingObject,
467 pub style: DrawingStyle,
468 pub locked: bool,
469 pub visible: bool,
470}
471
472#[derive(Debug, Clone, PartialEq)]
473pub enum DrawingDraft {
474 Trendline {
475 start: DrawingAnchor,
476 current: DrawingAnchor,
477 style: DrawingStyle,
478 },
479 Box {
480 start: DrawingAnchor,
481 current: DrawingAnchor,
482 style: DrawingStyle,
483 extend_right: bool,
484 },
485}
486
487impl DrawingDraft {
488 pub fn update_anchor(&mut self, anchor: DrawingAnchor) -> bool {
489 match self {
490 DrawingDraft::Trendline { start, current, .. }
491 | DrawingDraft::Box { start, current, .. } => {
492 if anchor.panel_id != start.panel_id {
493 return false;
494 }
495
496 *current = anchor;
497 }
498 }
499
500 true
501 }
502
503 pub fn tool(&self) -> DrawingTool {
504 match self {
505 Self::Trendline { .. } => DrawingTool::Trendline,
506 Self::Box { .. } => DrawingTool::Box,
507 }
508 }
509
510 pub fn style(&self) -> DrawingStyle {
511 match self {
512 Self::Trendline { style, .. } | Self::Box { style, .. } => *style,
513 }
514 }
515
516 pub fn preview_object(&self) -> DrawingObject {
517 match self {
518 Self::Trendline { start, current, .. } => DrawingObject::Trendline {
519 start: *start,
520 end: *current,
521 },
522 Self::Box {
523 start,
524 current,
525 extend_right,
526 ..
527 } => DrawingObject::normalized_box(*start, *current, *extend_right),
528 }
529 }
530
531 pub fn panel_id(&self) -> PanelId {
532 match self {
533 Self::Trendline { start, .. } | Self::Box { start, .. } => start.panel_id,
534 }
535 }
536
537 pub fn belongs_to_panel(&self, panel_id: PanelId) -> bool {
538 match self {
539 Self::Trendline { start, current, .. } | Self::Box { start, current, .. } => {
540 start.panel_id == panel_id || current.panel_id == panel_id
541 }
542 }
543 }
544
545 pub fn try_commit(self, end: DrawingAnchor) -> Option<(DrawingObject, DrawingStyle)> {
546 match self {
547 Self::Trendline { start, style, .. } => {
548 if end.panel_id != start.panel_id || !start.x.compatible(end.x) {
549 return None;
550 }
551
552 Some((DrawingObject::Trendline { start, end }, style))
553 }
554 Self::Box {
555 start,
556 style,
557 extend_right,
558 ..
559 } => {
560 if end.panel_id != start.panel_id || !start.x.compatible(end.x) {
561 return None;
562 }
563
564 Some((
565 DrawingObject::normalized_box(start, end, extend_right),
566 style,
567 ))
568 }
569 }
570 }
571}
572
573#[derive(Debug, Clone, Copy, PartialEq, Eq)]
574pub enum DrawingHandleKind {
575 TrendlineStart,
576 TrendlineEnd,
577 HorizontalLineStart,
578 BoxTopLeft,
579 BoxTopRight,
580 BoxBottomRight,
581 BoxBottomLeft,
582}
583
584#[derive(Debug, Clone, Copy, PartialEq, Eq)]
585pub enum DrawingDragTarget {
586 Translate,
587 Handle(DrawingHandleKind),
588}
589
590#[derive(Debug, Clone, Copy, PartialEq, Eq)]
591pub enum DrawingDragOperation {
592 Move,
593 Copy,
594}
595
596#[derive(Debug, Clone, Copy, PartialEq)]
597pub enum AxisLabelAnchor {
598 X { x: DrawingX },
599 Y { panel_id: PanelId, y_unit: YUnit },
600}
601
602impl DrawingObject {
603 fn normalized_box(
604 mut start: DrawingAnchor,
605 mut end: DrawingAnchor,
606 extend_right: bool,
607 ) -> Self {
608 if extend_right && end.x < start.x {
609 std::mem::swap(&mut start.x, &mut end.x);
610 }
611
612 Self::Box {
613 start,
614 end,
615 extend_right,
616 }
617 }
618
619 pub fn tool(&self) -> DrawingTool {
620 match self {
621 Self::Trendline { .. } => DrawingTool::Trendline,
622 Self::Box { .. } => DrawingTool::Box,
623 Self::HorizontalLine { .. } => DrawingTool::HorizontalLine,
624 Self::VerticalLine { .. } => DrawingTool::VerticalLine,
625 }
626 }
627
628 pub fn title(&self) -> &'static str {
629 match self {
630 Self::Box { .. } => "Box",
631 Self::Trendline { .. } => "Trendline",
632 Self::HorizontalLine { .. } => "Horizontal Line",
633 Self::VerticalLine { .. } => "Vertical Line",
634 }
635 }
636
637 pub fn panel_id(&self) -> Option<PanelId> {
638 match self {
639 Self::Trendline { start, .. } | Self::Box { start, .. } => Some(start.panel_id),
640 Self::HorizontalLine { panel_id, .. } => Some(*panel_id),
641 Self::VerticalLine { .. } => None,
642 }
643 }
644
645 pub fn handle_panel_id(&self) -> Option<PanelId> {
646 match self {
647 Self::Trendline { start, end } | Self::Box { start, end, .. } => {
648 (start.panel_id == end.panel_id).then_some(start.panel_id)
649 }
650 Self::HorizontalLine { panel_id, .. } => Some(*panel_id),
651 Self::VerticalLine { .. } => None,
652 }
653 }
654
655 pub fn axis_label_anchors(&self) -> Vec<AxisLabelAnchor> {
656 match self {
657 Self::Trendline { start, end } => vec![
658 AxisLabelAnchor::X { x: start.x },
659 AxisLabelAnchor::Y {
660 panel_id: start.panel_id,
661 y_unit: start.y_unit,
662 },
663 AxisLabelAnchor::X { x: end.x },
664 AxisLabelAnchor::Y {
665 panel_id: end.panel_id,
666 y_unit: end.y_unit,
667 },
668 ],
669 Self::Box {
670 start,
671 end,
672 extend_right,
673 } => {
674 let panel_id = start.panel_id;
675 let left = start.x.min(end.x);
676 let top = YUnit(start.y_unit.0.max(end.y_unit.0));
677 let bottom = YUnit(start.y_unit.0.min(end.y_unit.0));
678
679 let mut anchors = vec![
680 AxisLabelAnchor::X { x: left },
681 AxisLabelAnchor::Y {
682 panel_id,
683 y_unit: top,
684 },
685 AxisLabelAnchor::Y {
686 panel_id,
687 y_unit: bottom,
688 },
689 ];
690
691 if !extend_right {
692 anchors.push(AxisLabelAnchor::X {
693 x: start.x.max(end.x),
694 });
695 }
696
697 anchors
698 }
699 Self::HorizontalLine {
700 panel_id,
701 y_unit,
702 x,
703 ray,
704 } => {
705 let mut anchors = Vec::with_capacity(2);
706 if *ray {
707 anchors.push(AxisLabelAnchor::X { x: *x });
708 }
709 anchors.push(AxisLabelAnchor::Y {
710 panel_id: *panel_id,
711 y_unit: *y_unit,
712 });
713 anchors
714 }
715 Self::VerticalLine { x } => vec![AxisLabelAnchor::X { x: *x }],
716 }
717 }
718
719 pub fn belongs_to_panel(&self, panel_id: PanelId) -> bool {
720 match self {
721 Self::Trendline { start, end } | Self::Box { start, end, .. } => {
722 start.panel_id == panel_id || end.panel_id == panel_id
723 }
724 Self::HorizontalLine {
725 panel_id: drawing_panel,
726 ..
727 } => *drawing_panel == panel_id,
728 Self::VerticalLine { .. } => false,
729 }
730 }
731
732 pub fn coordinates_match(&self, x: DrawingX) -> bool {
733 match self {
734 Self::Trendline { start, end } | Self::Box { start, end, .. } => {
735 start.x.compatible(x) && end.x.compatible(x)
736 }
737 Self::HorizontalLine { x: origin, .. } | Self::VerticalLine { x: origin } => {
738 origin.compatible(x)
739 }
740 }
741 }
742
743 pub fn translated(&self, origin: DrawingAnchor, current: DrawingAnchor) -> Option<Self> {
744 let delta_y = current.y_unit.0.saturating_sub(origin.y_unit.0);
745 let shift = |anchor: DrawingAnchor| -> Option<DrawingAnchor> {
746 Some(DrawingAnchor {
747 x: anchor.x.translated(origin.x, current.x)?,
748 y_unit: Self::shift_y_by_delta(anchor.y_unit, delta_y),
749 ..anchor
750 })
751 };
752 Some(match self {
753 Self::Trendline { start, end } => Self::Trendline {
754 start: shift(*start)?,
755 end: shift(*end)?,
756 },
757 Self::Box {
758 start,
759 end,
760 extend_right,
761 } => Self::Box {
762 start: shift(*start)?,
763 end: shift(*end)?,
764 extend_right: *extend_right,
765 },
766 Self::HorizontalLine {
767 panel_id,
768 y_unit,
769 x,
770 ray,
771 } => Self::HorizontalLine {
772 panel_id: *panel_id,
773 y_unit: Self::shift_y_by_delta(*y_unit, delta_y),
774 x: x.translated(origin.x, current.x)?,
775 ray: *ray,
776 },
777 Self::VerticalLine { x } => Self::VerticalLine {
778 x: x.translated(origin.x, current.x)?,
779 },
780 })
781 }
782
783 pub fn handle_dragged(
784 &self,
785 handle: DrawingHandleKind,
786 origin: DrawingAnchor,
787 mut anchor: DrawingAnchor,
788 ) -> Option<Self> {
789 if !self.coordinates_match(anchor.x) || !origin.x.compatible(anchor.x) {
790 return None;
791 }
792 if matches!(anchor.x, DrawingX::Tick(_)) {
793 let x = match (self, handle) {
794 (Self::Trendline { start, .. }, DrawingHandleKind::TrendlineStart) => start.x,
795 (Self::Trendline { end, .. }, DrawingHandleKind::TrendlineEnd) => end.x,
796 (Self::HorizontalLine { x, .. }, DrawingHandleKind::HorizontalLineStart) => *x,
797 (
798 Self::Box { start, end, .. },
799 DrawingHandleKind::BoxTopLeft | DrawingHandleKind::BoxBottomLeft,
800 ) => start.x.min(end.x),
801 (
802 Self::Box { start, end, .. },
803 DrawingHandleKind::BoxTopRight | DrawingHandleKind::BoxBottomRight,
804 ) => start.x.max(end.x),
805 _ => return None,
806 };
807 anchor.x = x.translated(origin.x, anchor.x)?;
808 }
809 match (self, handle) {
810 (Self::Trendline { start, end }, DrawingHandleKind::TrendlineStart) => {
811 Some(Self::Trendline {
812 start: DrawingAnchor {
813 panel_id: start.panel_id,
814 ..anchor
815 },
816 end: *end,
817 })
818 }
819 (Self::Trendline { start, end }, DrawingHandleKind::TrendlineEnd) => {
820 Some(Self::Trendline {
821 start: *start,
822 end: DrawingAnchor {
823 panel_id: end.panel_id,
824 ..anchor
825 },
826 })
827 }
828 (
829 Self::HorizontalLine {
830 panel_id,
831 ray: true,
832 ..
833 },
834 DrawingHandleKind::HorizontalLineStart,
835 ) if anchor.panel_id == *panel_id => Some(Self::HorizontalLine {
836 panel_id: *panel_id,
837 y_unit: anchor.y_unit,
838 x: anchor.x,
839 ray: true,
840 }),
841 (
842 Self::Box {
843 start,
844 end,
845 extend_right,
846 },
847 corner,
848 ) => Self::resized_box(*start, *end, *extend_right, corner, anchor),
849 _ => None,
850 }
851 }
852
853 fn resized_box(
854 start: DrawingAnchor,
855 end: DrawingAnchor,
856 extend_right: bool,
857 corner: DrawingHandleKind,
858 anchor: DrawingAnchor,
859 ) -> Option<Self> {
860 if start.panel_id != end.panel_id {
861 return None;
862 }
863
864 let panel_id = start.panel_id;
865 if anchor.panel_id != panel_id {
866 return None;
867 }
868
869 if extend_right {
870 return Self::resized_extended_box(start, end, corner, anchor);
871 }
872
873 let mut left = if start.x <= end.x { start.x } else { end.x };
874 let mut right = if start.x <= end.x { end.x } else { start.x };
875
876 let mut top = start.y_unit.0.max(end.y_unit.0);
877 let mut bottom = start.y_unit.0.min(end.y_unit.0);
878
879 match corner {
880 DrawingHandleKind::BoxTopLeft => {
881 left = anchor.x;
882 top = anchor.y_unit.0;
883 }
884 DrawingHandleKind::BoxTopRight => {
885 right = anchor.x;
886 top = anchor.y_unit.0;
887 }
888 DrawingHandleKind::BoxBottomRight => {
889 right = anchor.x;
890 bottom = anchor.y_unit.0;
891 }
892 DrawingHandleKind::BoxBottomLeft => {
893 left = anchor.x;
894 bottom = anchor.y_unit.0;
895 }
896 DrawingHandleKind::TrendlineStart
897 | DrawingHandleKind::TrendlineEnd
898 | DrawingHandleKind::HorizontalLineStart => {
899 return None;
900 }
901 }
902
903 Some(Self::Box {
904 start: DrawingAnchor {
905 panel_id,
906 x: left,
907 y_unit: YUnit(top),
908 },
909 end: DrawingAnchor {
910 panel_id,
911 x: right,
912 y_unit: YUnit(bottom),
913 },
914 extend_right,
915 })
916 }
917
918 fn resized_extended_box(
919 start: DrawingAnchor,
920 end: DrawingAnchor,
921 corner: DrawingHandleKind,
922 anchor: DrawingAnchor,
923 ) -> Option<Self> {
924 let top = start.y_unit.0.max(end.y_unit.0);
925 let bottom = start.y_unit.0.min(end.y_unit.0);
926 let (top, bottom) = match corner {
927 DrawingHandleKind::BoxTopLeft => (anchor.y_unit.0, bottom),
928 DrawingHandleKind::BoxBottomLeft => (top, anchor.y_unit.0),
929 DrawingHandleKind::TrendlineStart
930 | DrawingHandleKind::TrendlineEnd
931 | DrawingHandleKind::HorizontalLineStart
932 | DrawingHandleKind::BoxTopRight
933 | DrawingHandleKind::BoxBottomRight => return None,
934 };
935
936 Some(Self::Box {
937 start: DrawingAnchor {
938 panel_id: start.panel_id,
939 x: anchor.x,
940 y_unit: YUnit(top),
941 },
942 end: DrawingAnchor {
943 panel_id: end.panel_id,
944 x: end.x,
945 y_unit: YUnit(bottom),
946 },
947 extend_right: true,
948 })
949 }
950
951 fn shift_y_by_delta(y_unit: YUnit, delta_y: i64) -> YUnit {
952 YUnit(y_unit.0.saturating_add(delta_y))
953 }
954}
955
956impl DrawingEntity {
957 pub fn belongs_to_panel(&self, panel_id: PanelId) -> bool {
958 self.object.belongs_to_panel(panel_id)
959 }
960}