1use crate::Chart;
4use crate::widget::SeriesId;
5
6use super::{ObjectTreeAction, ObjectTreePanel, SourceItem, SourceType};
7
8impl ObjectTreePanel {
9 pub fn apply_to_chart(
17 &mut self,
18 chart: &mut Chart,
19 action: &ObjectTreeAction,
20 sources: &[SourceItem],
21 ) -> bool {
22 let selected: Vec<_> = sources
23 .iter()
24 .filter(|s| self.state().is_selected(s.id))
25 .collect();
26 match action {
27 ObjectTreeAction::None => {}
28 ObjectTreeAction::CreateFolder(parent, name) => {
29 let name = name.trim();
30 if name.is_empty() || (!parent.is_empty() && !self.state().folders.contains(parent))
31 {
32 return false;
33 }
34 let mut path = parent.clone();
35 path.push(name.to_owned());
36 if !self.state_mut().folders.insert(path.clone()) {
37 return false;
38 }
39 self.state_mut().expanded_groups.insert("Drawings".into());
40 for depth in 1..=path.len() {
41 self.state_mut().collapsed_folders.remove(&path[..depth]);
42 }
43 let ids: Vec<_> = selected
44 .iter()
45 .filter(|s| s.is_drawing())
46 .map(|s| s.id)
47 .collect();
48 chart
49 .drawings_mut()
50 .edit_drawings(&ids, |d| d.folder = path.clone());
51 }
52 ObjectTreeAction::MoveToFolder(ids, path) => {
53 if !path.is_empty() && !self.state().folders.contains(path) {
54 return false;
55 }
56 let ids: Vec<_> = chart
57 .drawings()
58 .drawings
59 .iter()
60 .filter(|d| {
61 ids.contains(&d.id)
62 && d.folder != *path
63 && sources.iter().any(|s| s.id == d.id && s.is_drawing())
64 })
65 .map(|d| d.id)
66 .collect();
67 chart
68 .drawings_mut()
69 .edit_drawings(&ids, |d| d.folder = path.clone());
70 }
71 ObjectTreeAction::SetFolderVisible(path, value)
72 | ObjectTreeAction::SetFolderLocked(path, value) => {
73 if path.is_empty() || !self.state().folders.contains(path) {
74 return false;
75 }
76 let ids: Vec<_> = chart
77 .drawings()
78 .drawings
79 .iter()
80 .filter(|d| {
81 d.folder.starts_with(path)
82 && match action {
83 ObjectTreeAction::SetFolderVisible(_, _) => d.visible != *value,
84 _ => d.locked != *value,
85 }
86 })
87 .map(|d| d.id)
88 .collect();
89 chart.drawings_mut().edit_drawings(&ids, |d| match action {
90 ObjectTreeAction::SetFolderVisible(_, _) => d.visible = *value,
91 _ => d.locked = *value,
92 });
93 }
94 ObjectTreeAction::Rename(id, name) => {
95 if !sources.iter().any(|s| s.id == *id && s.is_drawing()) {
96 return false;
97 }
98 if !name.trim().is_empty() {
99 chart
100 .drawings_mut()
101 .edit_drawing(*id, |d| d.label = Some(name.clone()));
102 }
103 }
104 ObjectTreeAction::Select(id) => {
105 chart.clear_selection();
106 if sources.iter().any(|s| s.id == *id && s.is_drawing()) {
107 chart.drawings_mut().select(*id);
108 chart.reveal_drawing(*id);
109 } else {
110 chart.drawings_mut().deselect();
111 }
112 }
113 ObjectTreeAction::ClearSelection => {
114 chart.drawings_mut().deselect();
115 chart.clear_selection();
116 }
117 ObjectTreeAction::AddToSelection(_)
118 | ObjectTreeAction::RangeSelect(_)
119 | ObjectTreeAction::SelectAll => {
120 chart.clear_selection();
121 chart
122 .drawings_mut()
123 .select_drawings(selected.iter().filter(|s| s.is_drawing()).map(|s| s.id));
124 }
125 ObjectTreeAction::ShowAll | ObjectTreeAction::HideAll => {
126 let visible = matches!(action, ObjectTreeAction::ShowAll);
127 for source in sources {
128 set_visible(chart, source, visible);
129 }
130 }
131 ObjectTreeAction::HideSelected => {
132 for source in selected {
133 set_visible(chart, source, false);
134 }
135 }
136 ObjectTreeAction::ToggleVisibility(id) => {
137 let Some(source) = sources.iter().find(|s| s.id == *id) else {
138 return false;
139 };
140 if !set_visible(chart, source, !source.visible) {
141 return false;
142 }
143 }
144 ObjectTreeAction::ToggleLock(id) => {
145 chart
146 .drawings_mut()
147 .edit_drawing(*id, |d| d.locked = !d.locked);
148 }
149 ObjectTreeAction::LockAll | ObjectTreeAction::UnlockAll => {
150 let locked = matches!(action, ObjectTreeAction::LockAll);
151 let ids: Vec<_> = chart
152 .drawings()
153 .drawings
154 .iter()
155 .filter(|d| d.locked != locked)
156 .map(|d| d.id)
157 .collect();
158 for id in ids {
159 chart.drawings_mut().edit_drawing(id, |d| d.locked = locked);
160 }
161 }
162 ObjectTreeAction::Delete(id) | ObjectTreeAction::RemoveIndicator(id) => {
163 let Some(source) = sources.iter().find(|s| s.id == *id) else {
164 return false;
165 };
166 if !source.is_drawing() && !source.is_indicator() {
167 return false;
168 }
169 delete(chart, source);
170 }
171 ObjectTreeAction::DeleteSelected => {
172 for source in selected {
173 delete(chart, source);
174 }
175 self.state_mut().clear_selection();
176 chart.drawings_mut().deselect();
177 }
178 ObjectTreeAction::Duplicate(id) => {
179 let Some(source) = sources.iter().find(|s| s.id == *id) else {
180 return false;
181 };
182 if !source.is_drawing() && !source.is_indicator() {
183 return false;
184 }
185 duplicate(chart, source);
186 }
187 ObjectTreeAction::DuplicateSelected => {
188 for source in selected {
189 duplicate(chart, source);
190 }
191 }
192 ObjectTreeAction::MoveToPane(id, pane) => {
193 if !sources.iter().any(|s| s.id == *id && s.is_drawing())
194 || !chart.move_drawing_to_pane(*id, *pane)
195 {
196 return false;
197 }
198 }
199 ObjectTreeAction::BringToFront(id)
200 | ObjectTreeAction::SendToBack(id)
201 | ObjectTreeAction::MoveUp(id)
202 | ObjectTreeAction::MoveDown(id) => {
203 let mut order: Vec<_> = chart
204 .drawings()
205 .drawings
206 .iter()
207 .map(|d| (d.id, d.z_order))
208 .collect();
209 order.sort_by_key(|(_, z)| *z);
210 let Some(index) = order.iter().position(|(candidate, _)| candidate == id) else {
211 return false;
212 };
213 let other = match action {
214 ObjectTreeAction::BringToFront(_) => Some(order.len() - 1),
215 ObjectTreeAction::SendToBack(_) => Some(0),
216 ObjectTreeAction::MoveUp(_) => {
217 index.checked_add(1).filter(|next| *next < order.len())
218 }
219 _ => index.checked_sub(1),
220 };
221 if let Some(other) = other.filter(|other| *other != index) {
222 let target = order.remove(index);
223 order.insert(other, target);
224 let layers: Vec<_> = order
226 .into_iter()
227 .enumerate()
228 .filter_map(|(z, (id, _))| i32::try_from(z).ok().map(|z| (id, z)))
229 .collect();
230 chart.drawings_mut().reorder_drawings(*id, &layers);
231 }
232 }
233 ObjectTreeAction::ZoomTo(id) => {
234 if !sources.iter().any(|s| s.id == *id && s.is_drawing())
235 || !chart.drawings().drawings.iter().any(|d| d.id == *id)
236 {
237 return false;
238 }
239 chart.clear_selection();
240 chart.drawings_mut().select(*id);
241 chart.reveal_drawing(*id);
242 }
243 ObjectTreeAction::RemoveAllDrawings => chart.drawings_mut().clear_all(),
244 ObjectTreeAction::RemoveAllIndicators => {
245 let ids: Vec<SeriesId> = chart.indicators().map(|(id, _)| id).collect();
246 for id in ids {
247 chart.remove_series(id);
248 }
249 }
250 _ => return false,
251 }
252 true
253 }
254}
255
256fn set_visible(chart: &mut Chart, source: &SourceItem, visible: bool) -> bool {
257 if source.source_type == SourceType::DataSource && source.series_id == Some(SeriesId::MAIN) {
258 chart.set_main_series_visible(visible);
259 true
260 } else if source.is_indicator() {
261 source
262 .series_id
263 .is_some_and(|id| chart.set_indicator_visible(id, visible))
264 } else if source.is_drawing() {
265 chart
266 .drawings_mut()
267 .edit_drawing(source.id, |d| d.visible = visible);
268 true
269 } else {
270 false
271 }
272}
273
274fn delete(chart: &mut Chart, source: &SourceItem) {
275 if source.is_indicator() {
276 if let Some(id) = source.series_id {
277 chart.remove_series(id);
278 }
279 } else if source.is_drawing() {
280 chart.drawings_mut().delete_drawing(source.id);
281 }
282}
283
284fn duplicate(chart: &mut Chart, source: &SourceItem) {
285 if source.source_type == SourceType::Indicator {
286 let copy = chart
287 .indicators()
288 .find(|(id, _)| Some(*id) == source.series_id)
289 .map(|(id, i)| {
290 (
291 i.clone_box(),
292 chart.indicator_inputs(id).unwrap_or_default().to_owned(),
293 )
294 });
295 if let Some((copy, inputs)) = copy {
296 let id = chart.add_indicator(copy);
297 chart.set_indicator_inputs(id, inputs);
298 }
299 } else if source.is_drawing() {
300 chart.drawings_mut().duplicate_drawing(source.id);
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use crate::drawings::{ChartPoint, Drawing, DrawingToolType};
308 use crate::model::LogicalRange;
309 use crate::widget::test_frames::{chart, frame};
310
311 #[test]
312 fn tree_selection_reveals_drawings_without_refitting_scales() {
313 for tool in [
314 DrawingToolType::AnchoredVWAP,
315 DrawingToolType::AnchoredVolumeProfile,
316 ] {
317 for zoom in [false, true] {
318 for manual in [false, true] {
319 for (index, price, low, high) in [
321 (10.0, 150.0, 100.0, 200.0),
322 (70.0, 150.0, 100.0, 200.0),
323 (110.0, 150.0, 100.0, 200.0),
324 (70.0, 80.0, 75.0, 175.0),
325 (70.0, 220.0, 125.0, 225.0),
326 ] {
327 let ctx = egui::Context::default();
328 let mut chart = chart(&[100.0], &[200.0]);
329 frame(&ctx, &mut chart, 0.0, vec![]);
330 chart.set_visible_logical_range(LogicalRange::new(50.0, 89.0));
331 if manual {
332 chart.price_scale_mut().set_range(100.0, 200.0);
333 }
334 let mut drawing = Drawing::new(1, tool);
335 drawing.chart_points = vec![ChartPoint::new(index, price)];
336 drawing.completed = true;
337 chart.drawings_mut().add_synced_drawing(drawing).unwrap();
338 let id = chart.drawings().drawings[0].id;
339 let sources = [SourceItem::drawing(id, tool, egui::Color32::RED)];
340 let mut tree = ObjectTreePanel::new();
341 let action = if zoom {
342 ObjectTreeAction::ZoomTo(id)
343 } else {
344 ObjectTreeAction::Select(id)
345 };
346 let spacing = chart.get_time_scale_state().0;
347 assert!(tree.apply_to_chart(&mut chart, &action, &sources));
348 assert_eq!(chart.drawings().selected_ids().collect::<Vec<_>>(), [id]);
349 assert_eq!(chart.get_time_scale_state().0, spacing);
350 assert_eq!(chart.state().price_scale().is_manual(), manual);
351 let range = chart.visible_logical_range().unwrap();
352 assert!(
353 range.left <= index && index <= range.right,
354 "{action:?}: {range:?}, index {index}"
355 );
356 if index == 70.0 {
357 assert_eq!(range, LogicalRange::new(50.0, 89.0));
358 }
359 if manual {
360 assert_eq!(
361 chart.state().price_scale().range(),
362 crate::scales::PriceRange::new(low, high)
363 );
364 }
365 }
366 }
367 }
368 }
369 }
370
371 fn rows(chart: &Chart) -> Vec<SourceItem> {
373 const STUDIES: usize = usize::MAX / 2;
374 let mut main = SourceItem::data_source(usize::MAX, "AAPL").with_series_id(SeriesId::MAIN);
375 main.visible = chart.is_main_series_visible();
376 let mut rows = vec![main];
377 for (id, study) in chart.indicators() {
378 let item = SourceItem::indicator(STUDIES + id.0, study.name(), egui::Color32::GRAY)
379 .with_series_id(id);
380 rows.push(item);
381 }
382 for drawing in chart.drawings().listed_drawings() {
383 let mut item = SourceItem::drawing(drawing.id, drawing.tool_type, drawing.color32());
384 item.visible = drawing.visible;
385 item.z_index = drawing.z_order;
386 rows.push(item);
387 }
388 rows
389 }
390
391 fn apply(tree: &mut ObjectTreePanel, chart: &mut Chart, action: ObjectTreeAction) {
393 let rows = rows(chart);
394 chart.user_action(|chart| tree.apply_to_chart(chart, &action, &rows));
395 }
396
397 #[test]
398 fn layers_names_copies_and_visibility_apply_as_undoable_steps() {
399 let mut chart = chart(&[100.0], &[200.0]);
400 let mut tree = ObjectTreePanel::new();
401 for id in 1..=3 {
402 let mut drawing = Drawing::new(id, DrawingToolType::TrendLine);
403 drawing.chart_points = vec![ChartPoint::new(10.0, 100.0), ChartPoint::new(20.0, 200.0)];
404 drawing.completed = true;
405 chart.drawings_mut().add_synced_drawing(drawing).unwrap();
406 }
407 let ids: Vec<_> = chart.drawings().drawings.iter().map(|d| d.id).collect();
408 let z_order = |chart: &Chart| {
409 chart
410 .drawings()
411 .drawings
412 .iter()
413 .map(|d| d.z_order)
414 .collect::<Vec<_>>()
415 };
416 let painted = |chart: &Chart| {
417 let mut order: Vec<_> = chart
418 .drawings()
419 .drawings
420 .iter()
421 .map(|d| (d.z_order, d.id))
422 .collect();
423 order.sort_by_key(|(z, _)| *z);
424 order.into_iter().map(|(_, id)| id).collect::<Vec<_>>()
425 };
426 apply(
428 &mut tree,
429 &mut chart,
430 ObjectTreeAction::ToggleVisibility(usize::MAX),
431 );
432 assert!(!chart.is_main_series_visible());
433 assert!(chart.undo());
434 assert!(chart.is_main_series_visible());
435 apply(
436 &mut tree,
437 &mut chart,
438 ObjectTreeAction::ToggleVisibility(ids[0]),
439 );
440 assert!(!chart.drawings().drawings[0].visible);
441 assert!(chart.undo());
442 assert!(chart.drawings().drawings[0].visible);
443 for drawing in &mut chart.drawings_mut().drawings {
445 drawing.z_order = 0;
446 }
447 for (action, expected) in [
448 (
449 ObjectTreeAction::MoveUp(ids[0]),
450 vec![ids[1], ids[0], ids[2]],
451 ),
452 (ObjectTreeAction::MoveDown(ids[0]), ids.clone()),
453 (
454 ObjectTreeAction::BringToFront(ids[0]),
455 vec![ids[1], ids[2], ids[0]],
456 ),
457 (ObjectTreeAction::SendToBack(ids[0]), ids.clone()),
458 ] {
459 let before = z_order(&chart);
460 apply(&mut tree, &mut chart, action);
461 let after = z_order(&chart);
462 assert!(chart.undo());
463 assert_eq!(z_order(&chart), before);
464 assert!(chart.redo());
465 assert_eq!(z_order(&chart), after);
466 assert_eq!(painted(&chart), expected);
467 }
468 apply(
469 &mut tree,
470 &mut chart,
471 ObjectTreeAction::Rename(ids[0], "Support".into()),
472 );
473 assert_eq!(
474 chart.drawings().drawings[0].label.as_deref(),
475 Some("Support")
476 );
477 assert!(chart.undo());
478 assert_eq!(chart.drawings().drawings[0].label, None);
479 apply(&mut tree, &mut chart, ObjectTreeAction::Duplicate(ids[0]));
480 assert_eq!(chart.drawings().drawings.len(), 4);
481 assert!(chart.undo());
482 assert_eq!(chart.drawings().drawings.len(), 3);
483 let study = chart.add_indicator(Box::new(crate::studies::SMA::new(2)));
485 chart.set_indicator_inputs(study, "2 close");
486 let before: Vec<_> = chart.indicators().map(|(id, _)| id).collect();
487 apply(
488 &mut tree,
489 &mut chart,
490 ObjectTreeAction::Duplicate(usize::MAX / 2 + study.0),
491 );
492 let copy = chart
493 .indicators()
494 .map(|(id, _)| id)
495 .find(|id| !before.contains(id))
496 .unwrap();
497 assert_eq!(chart.indicator_inputs(copy), Some("2 close"));
498 }
499}