1use fret_core::time::{Duration, Instant};
2use std::collections::BTreeMap;
3use std::sync::{Arc, Mutex};
4
5use delinea::engine::model::{ChartPatch, PatchMode};
6use delinea::engine::window::DataWindow;
7use delinea::marks::{MarkKind, MarkPayloadRef, MarkTree};
8use delinea::tooltip::TooltipOutput;
9use delinea::{Action, ChartEngine, WorkBudget};
10use fret_canvas::ui::{
11 CanvasToolDownResult, CanvasToolEntry, CanvasToolHandlers, CanvasToolId, CanvasToolRouterProps,
12 OnCanvasToolPinch, OnCanvasToolPointerDown, OnCanvasToolPointerMove, OnCanvasToolPointerUp,
13 OnCanvasToolWheel, PanZoomCanvasPaintCx, PanZoomCanvasSurfacePanelProps,
14 canvas_tool_router_panel,
15};
16use fret_core::{
17 Color, Corners, DrawOrder, Edges, KeyCode, MouseButton, PathCommand, PathStyle, Point, Px,
18 Rect, Size, StrokeStyle,
19};
20use fret_runtime::Model;
21use fret_ui::action::OnKeyDown;
22use fret_ui::canvas::CanvasPainter;
23use fret_ui::element::{AnyElement, CanvasProps, FocusScopeProps, Length, PointerRegionProps};
24use fret_ui::{ElementContext, UiHost};
25
26use crate::input_map::{ChartInputMap, ModifierKey};
27use crate::retained::ChartStyle;
28use crate::{DefaultTooltipFormatter, TooltipFormatter, TooltipTextLine};
29
30use super::legend_overlay::{LegendOverlayState, LegendSeriesEntry, legend_overlay_tool};
31use super::tooltip_overlay::{AxisPointerLabelOverlay, TooltipOverlayState, tooltip_overlay_tool};
32
33#[derive(Debug, Default)]
34struct NullTextMeasurer;
35
36impl delinea::text::TextMeasurer for NullTextMeasurer {
37 fn measure(
38 &mut self,
39 _text: delinea::ids::StringId,
40 _style: delinea::text::TextStyleId,
41 ) -> delinea::text::TextMetrics {
42 delinea::text::TextMetrics::default()
43 }
44}
45
46#[derive(Debug, Clone, Copy)]
47struct ChartPanDrag {
48 start_pos: Point,
49 x_axis: delinea::AxisId,
50 y_axis: delinea::AxisId,
51 start_x: DataWindow,
52 start_y: DataWindow,
53}
54
55fn default_chart_input_map_safe() -> ChartInputMap {
56 let mut map = ChartInputMap::default();
57 map.wheel_zoom_mod = Some(ModifierKey::Ctrl);
58 map
59}
60
61fn primary_axes(engine: &ChartEngine) -> Option<(delinea::AxisId, delinea::AxisId)> {
62 let model = engine.model();
63 for id in &model.series_order {
64 let s = model.series.get(id)?;
65 if s.visible {
66 return Some((s.x_axis, s.y_axis));
67 }
68 }
69 None
70}
71
72fn fallback_window() -> DataWindow {
73 DataWindow { min: 0.0, max: 1.0 }
74}
75
76fn window_for_axis_x(engine: &ChartEngine, axis: delinea::AxisId) -> DataWindow {
77 engine
78 .output()
79 .axis_windows
80 .get(&axis)
81 .copied()
82 .unwrap_or_else(fallback_window)
83}
84
85fn window_for_axis_y(engine: &ChartEngine, axis: delinea::AxisId) -> DataWindow {
86 engine
87 .output()
88 .axis_windows
89 .get(&axis)
90 .copied()
91 .unwrap_or_else(fallback_window)
92}
93
94fn paint_color(style: ChartStyle, paint: delinea::PaintId) -> Color {
95 let palette = &style.series_palette;
96 palette[(paint.0 as usize) % palette.len()]
97}
98
99fn series_color(style: ChartStyle, series: delinea::SeriesId) -> Color {
100 let palette = &style.series_palette;
101 palette[(series.0 as usize) % palette.len()]
102}
103
104#[track_caller]
105fn ensure_engine_model<H: UiHost>(
106 cx: &mut ElementContext<'_, H>,
107 controlled: Option<Model<ChartEngine>>,
108 spec: delinea::ChartSpec,
109) -> Model<ChartEngine> {
110 if let Some(model) = controlled {
111 return model;
112 }
113
114 let mut spec = spec;
115 spec.axis_pointer.get_or_insert_with(Default::default);
116 cx.local_model(|| ChartEngine::new(spec).expect("chart spec should be valid"))
117}
118
119#[derive(Debug, Clone)]
120struct MarksCache {
121 marks_rev: delinea::ids::Revision,
122 output_rev: delinea::ids::Revision,
123 marks: Arc<MarkTree>,
124 axis_pointer: Option<AxisPointerPaintData>,
125 hover_point_px: Option<Point>,
126}
127
128impl Default for MarksCache {
129 fn default() -> Self {
130 Self {
131 marks_rev: delinea::ids::Revision::default(),
132 output_rev: delinea::ids::Revision::default(),
133 marks: Arc::new(MarkTree::default()),
134 axis_pointer: None,
135 hover_point_px: None,
136 }
137 }
138}
139
140#[derive(Debug, Clone, Copy)]
141struct AxisPointerPaintData {
142 crosshair_px: Point,
143 shadow_rect_px: Option<Rect>,
144 draw_x: bool,
145 draw_y: bool,
146}
147
148#[derive(Clone)]
149pub struct ChartCanvasPanelProps {
150 pub pointer_region: PointerRegionProps,
151 pub canvas: CanvasProps,
152
153 pub engine: Option<Model<ChartEngine>>,
155 pub spec: delinea::ChartSpec,
156
157 pub tooltip_formatter: Option<Arc<dyn TooltipFormatter>>,
161
162 pub input_map: ChartInputMap,
165
166 pub style: ChartStyle,
167}
168
169impl ChartCanvasPanelProps {
170 pub fn new(spec: delinea::ChartSpec) -> Self {
171 Self {
172 pointer_region: PointerRegionProps::default(),
173 canvas: CanvasProps::default(),
174 engine: None,
175 spec,
176 tooltip_formatter: None,
177 input_map: default_chart_input_map_safe(),
178 style: ChartStyle::default(),
179 }
180 }
181}
182
183#[track_caller]
184pub fn chart_canvas_panel<H: UiHost>(
185 cx: &mut ElementContext<'_, H>,
186 mut props: ChartCanvasPanelProps,
187) -> AnyElement {
188 props.pointer_region.layout.size.width = Length::Fill;
189 props.pointer_region.layout.size.height = Length::Fill;
190 props.canvas.layout.size.width = Length::Fill;
191 props.canvas.layout.size.height = Length::Fill;
192
193 let engine = ensure_engine_model(cx, props.engine.clone(), props.spec.clone());
194
195 let pan_drag: Model<Option<ChartPanDrag>> = cx.local_model(|| None::<ChartPanDrag>);
197
198 let legend_state: Arc<Mutex<LegendOverlayState>> = cx.slot_state(
199 || Arc::new(Mutex::new(LegendOverlayState::default())),
200 |st| st.clone(),
201 );
202 let tooltip_state: Arc<Mutex<TooltipOverlayState>> = cx.slot_state(
203 || Arc::new(Mutex::new(TooltipOverlayState::default())),
204 |st| st.clone(),
205 );
206
207 let default_tooltip_formatter: Arc<dyn TooltipFormatter> = cx.slot_state(
208 || Arc::new(DefaultTooltipFormatter) as Arc<dyn TooltipFormatter>,
209 |st| st.clone(),
210 );
211 let tooltip_formatter: Arc<dyn TooltipFormatter> = props
212 .tooltip_formatter
213 .clone()
214 .unwrap_or(default_tooltip_formatter);
215
216 let bounds = cx.bounds;
218 let mut unfinished = false;
219
220 let marks_cache_slot = cx.slot_id();
221 let (prev_marks_rev, prev_output_rev) =
222 cx.state_for(marks_cache_slot, MarksCache::default, |cache| {
223 (cache.marks_rev, cache.output_rev)
224 });
225
226 let mut marks_rev = prev_marks_rev;
227 let mut output_rev = prev_output_rev;
228 let mut output_marks: Option<Arc<MarkTree>> = None;
229
230 let mut legend_series: Vec<LegendSeriesEntry> = Vec::new();
231 let mut series_rank_by_id: BTreeMap<delinea::SeriesId, usize> = BTreeMap::default();
232 let mut axis_pointer_output: Option<delinea::engine::AxisPointerOutput> = None;
233 let mut axis_pointer_labels: Vec<AxisPointerLabelOverlay> = Vec::new();
234 let mut tooltip_lines: Vec<TooltipTextLine> = Vec::new();
235
236 let mut axis_pointer: Option<AxisPointerPaintData> = None;
237 let mut hover_point_px: Option<Point> = None;
238
239 let tooltip_formatter_c = tooltip_formatter.clone();
240 let _ = engine.update(cx.app, |engine, _cx| {
241 if engine.model().viewport != Some(bounds) {
242 let _ = engine.apply_patch(
243 ChartPatch {
244 viewport: Some(Some(bounds)),
245 ..ChartPatch::default()
246 },
247 PatchMode::Merge,
248 );
249 }
250
251 let mut measurer = NullTextMeasurer;
252 let start = Instant::now();
253 let mut steps_ran = 0u32;
254 let mut still_unfinished = true;
255 while still_unfinished && steps_ran < 8 && start.elapsed() < Duration::from_millis(4) {
256 let budget = WorkBudget::new(262_144, 0, 32);
257 let step = engine.step(&mut measurer, budget);
258 match step {
259 Ok(step) => {
260 still_unfinished = step.unfinished;
261 }
262 Err(_) => {
263 still_unfinished = false;
264 }
265 }
266 steps_ran = steps_ran.saturating_add(1);
267 }
268
269 unfinished = still_unfinished;
270
271 let output = engine.output();
272 output_rev = output.revision;
273 marks_rev = output.marks.revision;
274
275 if marks_rev != prev_marks_rev {
276 output_marks = Some(Arc::new(output.marks.clone()));
277 }
278
279 let model = engine.model();
280 series_rank_by_id.clear();
281 legend_series = model
282 .series_in_order()
283 .enumerate()
284 .map(|(order, s)| LegendSeriesEntry {
285 id: s.id,
286 order,
287 label: s
288 .name
289 .clone()
290 .unwrap_or_else(|| format!("Series {}", s.id.0))
291 .into(),
292 visible: s.visible,
293 })
294 .collect();
295 for s in &legend_series {
296 series_rank_by_id.insert(s.id, s.order);
297 }
298
299 axis_pointer_output = output.axis_pointer.clone();
300 axis_pointer_labels.clear();
301 tooltip_lines.clear();
302 if let Some(axis_pointer) = axis_pointer_output.as_ref() {
303 tooltip_lines =
304 tooltip_formatter_c.format_axis_pointer(engine, &output.axis_windows, axis_pointer);
305
306 if let Some(pointer_model) = model.axis_pointer.as_ref()
307 && pointer_model.label.show
308 {
309 let default_tooltip_spec = delinea::TooltipSpecV1::default();
310 let tooltip_spec = model.tooltip.as_ref().unwrap_or(&default_tooltip_spec);
311 let template = pointer_model.label.template.as_str();
312
313 let mut push_label_for_axis =
314 |axis_id: delinea::AxisId, axis_kind: delinea::AxisKind, axis_value: f64| {
315 let axis_window = output
316 .axis_windows
317 .get(&axis_id)
318 .copied()
319 .unwrap_or_default();
320 let axis_name = model
321 .axes
322 .get(&axis_id)
323 .and_then(|a| a.name.as_deref())
324 .unwrap_or("");
325 let value_text = if axis_value.is_finite() {
326 delinea::engine::axis::format_value_for(
327 model,
328 axis_id,
329 axis_window,
330 axis_value,
331 )
332 } else {
333 tooltip_spec.missing_value.clone()
334 };
335 let label_text = if template == "{value}" {
336 value_text
337 } else {
338 template
339 .replace("{value}", &value_text)
340 .replace("{axis_name}", axis_name)
341 };
342 axis_pointer_labels.push(AxisPointerLabelOverlay {
343 axis_kind,
344 text: label_text.into(),
345 });
346 };
347
348 match &axis_pointer.tooltip {
349 delinea::TooltipOutput::Axis(axis) => {
350 push_label_for_axis(axis.axis, axis.axis_kind, axis.axis_value);
351 }
352 delinea::TooltipOutput::Item(item) => {
353 push_label_for_axis(item.x_axis, delinea::AxisKind::X, item.x_value);
354 push_label_for_axis(item.y_axis, delinea::AxisKind::Y, item.y_value);
355 }
356 }
357 }
358 }
359
360 if output_rev != prev_output_rev {
361 hover_point_px = output.hover.map(|hit| hit.point_px);
362
363 if let Some(axis_pointer_out) = output.axis_pointer.as_ref() {
364 let (draw_x, draw_y) = match &axis_pointer_out.tooltip {
365 TooltipOutput::Axis(axis) => match axis.axis_kind {
366 delinea::AxisKind::X => (true, false),
367 delinea::AxisKind::Y => (false, true),
368 },
369 TooltipOutput::Item(_) => (true, true),
370 };
371
372 axis_pointer = Some(AxisPointerPaintData {
373 crosshair_px: axis_pointer_out.crosshair_px,
374 shadow_rect_px: axis_pointer_out.shadow_rect_px,
375 draw_x,
376 draw_y,
377 });
378 } else {
379 axis_pointer = None;
380 }
381 }
382 });
383
384 if let Ok(mut st) = legend_state.lock() {
385 st.sync_series(legend_series);
386 }
387 if let Ok(mut st) = tooltip_state.lock() {
388 st.axis_pointer = axis_pointer_output;
389 st.axis_pointer_labels = std::mem::take(&mut axis_pointer_labels);
390 st.lines = tooltip_lines;
391 st.series_rank_by_id = series_rank_by_id;
392 }
393
394 let (cache, axis_pointer, hover_point_px) =
395 cx.state_for(marks_cache_slot, MarksCache::default, |cache| {
396 if cache.marks_rev != marks_rev
397 && let Some(marks) = output_marks.clone()
398 {
399 cache.marks_rev = marks_rev;
400 cache.marks = marks;
401 }
402
403 if cache.output_rev != output_rev {
404 cache.output_rev = output_rev;
405 cache.axis_pointer = axis_pointer;
406 cache.hover_point_px = hover_point_px;
407 }
408
409 (
410 cache.marks.clone(),
411 cache.axis_pointer,
412 cache.hover_point_px,
413 )
414 });
415
416 let style = props.style;
417 let engine_c = engine.clone();
418 let input_map = props.input_map;
419
420 let pan_drag_down = pan_drag.clone();
421 let on_pan_down: OnCanvasToolPointerDown = Arc::new(move |host, _action_cx, tool_cx, down| {
422 if !input_map.pan.matches(down.button, down.modifiers) {
423 return CanvasToolDownResult::unhandled();
424 }
425 if !tool_cx.bounds.contains(down.position) {
426 return CanvasToolDownResult::unhandled();
427 }
428
429 let Some((x_axis, y_axis)) = host
430 .models_mut()
431 .read(&engine_c, primary_axes)
432 .ok()
433 .flatten()
434 else {
435 return CanvasToolDownResult::unhandled();
436 };
437
438 let (start_x, start_y) = host
439 .models_mut()
440 .read(&engine_c, |engine| {
441 (
442 window_for_axis_x(engine, x_axis),
443 window_for_axis_y(engine, y_axis),
444 )
445 })
446 .ok()
447 .unwrap_or((fallback_window(), fallback_window()));
448
449 let _ = host.models_mut().update(&pan_drag_down, |st| {
450 *st = Some(ChartPanDrag {
451 start_pos: down.position,
452 x_axis,
453 y_axis,
454 start_x,
455 start_y,
456 });
457 });
458
459 CanvasToolDownResult::activate_and_capture()
460 });
461
462 let pan_drag_move = pan_drag.clone();
463 let engine_c = engine.clone();
464 let on_pan_move: OnCanvasToolPointerMove = Arc::new(move |host, action_cx, tool_cx, mv| {
465 let Some(drag) = host
466 .models_mut()
467 .read(&pan_drag_move, |st| *st)
468 .ok()
469 .flatten()
470 else {
471 return false;
472 };
473
474 let width = tool_cx.bounds.size.width.0;
475 let height = tool_cx.bounds.size.height.0;
476 if width <= 0.0 || height <= 0.0 {
477 return false;
478 }
479
480 let dx = mv.position.x.0 - drag.start_pos.x.0;
481 let dy = mv.position.y.0 - drag.start_pos.y.0;
482
483 let _ = host.models_mut().update(&engine_c, |engine| {
484 engine.apply_action(Action::PanDataWindowXFromBase {
485 axis: drag.x_axis,
486 base: drag.start_x,
487 delta_px: dx,
488 viewport_span_px: width,
489 });
490 engine.apply_action(Action::PanDataWindowYFromBase {
491 axis: drag.y_axis,
492 base: drag.start_y,
493 delta_px: -dy,
494 viewport_span_px: height,
495 });
496 });
497
498 host.request_redraw(action_cx.window);
499 true
500 });
501
502 let pan_drag_up = pan_drag.clone();
503 let on_pan_up: OnCanvasToolPointerUp = Arc::new(move |host, _action_cx, _tool_cx, _up| {
504 let _ = host.models_mut().update(&pan_drag_up, |st| *st = None);
505 true
506 });
507
508 let engine_c = engine.clone();
509 let on_hover_move: OnCanvasToolPointerMove = Arc::new(move |host, action_cx, _tool_cx, mv| {
510 let _ = host.models_mut().update(&engine_c, |engine| {
511 engine.apply_action(Action::HoverAt { point: mv.position });
512 });
513 host.request_redraw(action_cx.window);
514 true
515 });
516
517 let engine_c = engine.clone();
518 let input_map_c = input_map;
519 let on_wheel_zoom: OnCanvasToolWheel = Arc::new(move |host, action_cx, tool_cx, wheel| {
520 let delta_y = wheel.delta.y.0;
521 if !delta_y.is_finite() {
522 return false;
523 }
524
525 if let Some(required) = input_map_c.wheel_zoom_mod
526 && !required.is_pressed(wheel.modifiers)
527 {
528 return false;
529 }
530
531 let width = tool_cx.bounds.size.width.0;
532 let height = tool_cx.bounds.size.height.0;
533 if width <= 0.0 || height <= 0.0 {
534 return false;
535 }
536
537 let Some((x_axis, y_axis)) = host
538 .models_mut()
539 .read(&engine_c, primary_axes)
540 .ok()
541 .flatten()
542 else {
543 return false;
544 };
545
546 let (base_x, base_y) = host
547 .models_mut()
548 .read(&engine_c, |engine| {
549 (
550 window_for_axis_x(engine, x_axis),
551 window_for_axis_y(engine, y_axis),
552 )
553 })
554 .ok()
555 .unwrap_or((fallback_window(), fallback_window()));
556
557 let log2_scale = delta_y * 0.0025;
559
560 let local_x = (wheel.position.x.0 - tool_cx.bounds.origin.x.0).clamp(0.0, width);
561 let local_y = (wheel.position.y.0 - tool_cx.bounds.origin.y.0).clamp(0.0, height);
562 let center_x = local_x;
563 let center_y_from_bottom = height - local_y;
564
565 let _ = host.models_mut().update(&engine_c, |engine| {
566 engine.apply_action(Action::ZoomDataWindowXFromBase {
567 axis: x_axis,
568 base: base_x,
569 center_px: center_x,
570 log2_scale,
571 viewport_span_px: width,
572 });
573 engine.apply_action(Action::ZoomDataWindowYFromBase {
574 axis: y_axis,
575 base: base_y,
576 center_px: center_y_from_bottom,
577 log2_scale,
578 viewport_span_px: height,
579 });
580 });
581
582 host.request_redraw(action_cx.window);
583 true
584 });
585
586 let legend_tool = legend_overlay_tool(engine.clone(), legend_state.clone(), style);
587 let tooltip_tool = tooltip_overlay_tool(tooltip_state.clone(), style);
588
589 let engine_c = engine.clone();
590 let on_pinch_zoom: OnCanvasToolPinch = Arc::new(move |host, action_cx, tool_cx, pinch| {
591 if !pinch.delta.is_finite() {
592 return false;
593 }
594
595 let width = tool_cx.bounds.size.width.0;
596 let height = tool_cx.bounds.size.height.0;
597 if width <= 0.0 || height <= 0.0 {
598 return false;
599 }
600
601 let Some((x_axis, y_axis)) = host
602 .models_mut()
603 .read(&engine_c, primary_axes)
604 .ok()
605 .flatten()
606 else {
607 return false;
608 };
609
610 let (base_x, base_y) = host
611 .models_mut()
612 .read(&engine_c, |engine| {
613 (
614 window_for_axis_x(engine, x_axis),
615 window_for_axis_y(engine, y_axis),
616 )
617 })
618 .ok()
619 .unwrap_or((fallback_window(), fallback_window()));
620
621 let delta = pinch.delta.clamp(-0.95, 10.0);
623 let factor = (1.0 + delta).max(0.01);
624 let log2_scale = factor.log2();
625 if !log2_scale.is_finite() || log2_scale.abs() <= 1.0e-9 {
626 return false;
627 }
628
629 let local_x = (pinch.position.x.0 - tool_cx.bounds.origin.x.0).clamp(0.0, width);
630 let local_y = (pinch.position.y.0 - tool_cx.bounds.origin.y.0).clamp(0.0, height);
631 let center_x = local_x;
632 let center_y_from_bottom = height - local_y;
633
634 let _ = host.models_mut().update(&engine_c, |engine| {
635 engine.apply_action(Action::ZoomDataWindowXFromBase {
636 axis: x_axis,
637 base: base_x,
638 center_px: center_x,
639 log2_scale,
640 viewport_span_px: width,
641 });
642 engine.apply_action(Action::ZoomDataWindowYFromBase {
643 axis: y_axis,
644 base: base_y,
645 center_px: center_y_from_bottom,
646 log2_scale,
647 viewport_span_px: height,
648 });
649 });
650
651 host.request_redraw(action_cx.window);
652 true
653 });
654
655 let tools = vec![
656 legend_tool,
657 tooltip_tool,
658 CanvasToolEntry {
659 id: CanvasToolId::new(1),
660 priority: 100,
661 handlers: CanvasToolHandlers {
662 on_pointer_down: Some(on_pan_down),
663 on_pointer_move: Some(on_pan_move),
664 on_pointer_up: Some(on_pan_up),
665 ..Default::default()
666 },
667 },
668 CanvasToolEntry {
669 id: CanvasToolId::new(2),
670 priority: 50,
671 handlers: CanvasToolHandlers {
672 on_wheel: Some(on_wheel_zoom),
673 ..Default::default()
674 },
675 },
676 CanvasToolEntry {
677 id: CanvasToolId::new(4),
678 priority: 50,
679 handlers: CanvasToolHandlers {
680 on_pinch: Some(on_pinch_zoom),
681 ..Default::default()
682 },
683 },
684 CanvasToolEntry {
685 id: CanvasToolId::new(3),
686 priority: -10,
687 handlers: CanvasToolHandlers {
688 on_pointer_move: Some(on_hover_move),
689 ..Default::default()
690 },
691 },
692 ];
693
694 let mut pan_zoom = PanZoomCanvasSurfacePanelProps::default();
695 pan_zoom.pointer_region = props.pointer_region;
696 pan_zoom.canvas = props.canvas;
697
698 pan_zoom.pan_button = MouseButton::Other(999);
700 pan_zoom.min_zoom = 1.0;
701 pan_zoom.max_zoom = 1.0;
702
703 let router_props = CanvasToolRouterProps {
704 pan_zoom,
705 active_tool: None,
706 };
707
708 let marks = cache;
709 let paint = move |painter: &mut CanvasPainter<'_>, paint_cx: PanZoomCanvasPaintCx| {
710 if unfinished {
711 painter.request_animation_frame();
712 }
713
714 let bounds = painter.bounds();
715
716 if let Some(background) = style.background {
718 painter.scene().push(fret_core::SceneOp::Quad {
719 order: DrawOrder(style.draw_order.0.saturating_sub(1)),
720 rect: bounds,
721 background: fret_core::Paint::Solid(background).into(),
722 border: Edges::all(Px(0.0)),
723 border_paint: fret_core::Paint::TRANSPARENT.into(),
724
725 corner_radii: Corners::all(Px(0.0)),
726 });
727 }
728
729 let viewport = bounds;
730 painter.with_clip_rect(viewport, |painter| {
731 let marks = &*marks;
732 let arena = &marks.arena;
733
734 for node in &marks.nodes {
735 match (node.kind, &node.payload) {
736 (MarkKind::Polyline, MarkPayloadRef::Polyline(poly)) => {
737 let start = poly.points.start;
738 let end = poly.points.end;
739 if end <= start || end > arena.points.len() {
740 continue;
741 }
742
743 let mut commands: Vec<PathCommand> =
744 Vec::with_capacity((end - start).saturating_add(1));
745 for (i, p) in arena.points[start..end].iter().enumerate() {
746 if i == 0 {
747 commands.push(PathCommand::MoveTo(*p));
748 } else {
749 commands.push(PathCommand::LineTo(*p));
750 }
751 }
752 if commands.len() < 2 {
753 continue;
754 }
755
756 let stroke_width = poly
757 .stroke
758 .as_ref()
759 .map(|(_, s)| s.width)
760 .unwrap_or(style.stroke_width);
761 let stroke_color = if let Some((paint, _)) = &poly.stroke {
762 paint_color(style, *paint)
763 } else if let Some(series) = node.source_series {
764 series_color(style, series)
765 } else {
766 style.stroke_color
767 };
768
769 let key = node.id.0;
770 painter.path(
771 key,
772 DrawOrder(style.draw_order.0.saturating_add(node.order.0)),
773 Point::new(Px(0.0), Px(0.0)),
774 &commands,
775 PathStyle::Stroke(StrokeStyle {
776 width: stroke_width,
777 }),
778 stroke_color,
779 paint_cx.raster_scale_factor,
780 );
781 }
782 (MarkKind::Rect, MarkPayloadRef::Rect(rects)) => {
783 let start = rects.rects.start;
784 let end = rects.rects.end;
785 if end <= start || end > arena.rects.len() {
786 continue;
787 }
788
789 let stroke_width = rects
790 .stroke
791 .as_ref()
792 .map(|(_, s)| s.width)
793 .filter(|w| w.0.is_finite() && w.0 > 0.0)
794 .unwrap_or(Px(0.0));
795
796 for rect in &arena.rects[start..end] {
797 let mut background = Color::TRANSPARENT;
798 if let Some(paint) = rects.fill {
799 background = paint_color(style, paint);
800 } else if let Some(series) = node.source_series {
801 background = series_color(style, series);
802 }
803 background.a *= rects.opacity_mul.unwrap_or(1.0);
804
805 let border_color = if stroke_width.0 > 0.0 {
806 background
807 } else {
808 Color::TRANSPARENT
809 };
810
811 painter.scene().push(fret_core::SceneOp::Quad {
812 order: DrawOrder(style.draw_order.0.saturating_add(node.order.0)),
813 rect: *rect,
814 background: fret_core::Paint::Solid(background).into(),
815 border: Edges::all(stroke_width),
816 border_paint: fret_core::Paint::Solid(border_color).into(),
817 corner_radii: Corners::all(Px(0.0)),
818 });
819 }
820 }
821 (MarkKind::Points, MarkPayloadRef::Points(points)) => {
822 let start = points.points.start;
823 let end = points.points.end;
824 if end <= start || end > arena.points.len() {
825 continue;
826 }
827
828 let base_point_r = style.scatter_point_radius.0.max(1.0);
829 let stroke_width = points
830 .stroke
831 .as_ref()
832 .map(|(_, s)| s.width)
833 .filter(|w| w.0.is_finite() && w.0 > 0.0)
834 .unwrap_or(Px(0.0));
835
836 for p in &arena.points[start..end] {
837 let radius_mul = points
838 .radius_mul
839 .filter(|v| v.is_finite() && *v > 0.0)
840 .unwrap_or(1.0);
841 let point_r = (base_point_r * radius_mul).max(1.0);
842
843 let mut fill = style.stroke_color;
844 if let Some(paint) = points.fill {
845 fill = paint_color(style, paint);
846 fill.a *= style.scatter_fill_alpha;
847 } else if let Some(series) = node.source_series {
848 fill = series_color(style, series);
849 fill.a *= style.scatter_fill_alpha;
850 }
851 fill.a *= points.opacity_mul.unwrap_or(1.0);
852
853 let border_color = if stroke_width.0 > 0.0 {
854 fill
855 } else {
856 Color::TRANSPARENT
857 };
858
859 painter.scene().push(fret_core::SceneOp::Quad {
860 order: DrawOrder(style.draw_order.0.saturating_add(node.order.0)),
861 rect: Rect::new(
862 Point::new(Px(p.x.0 - point_r), Px(p.y.0 - point_r)),
863 Size::new(Px(2.0 * point_r), Px(2.0 * point_r)),
864 ),
865 background: fret_core::Paint::Solid(fill).into(),
866
867 border: Edges::all(stroke_width),
868 border_paint: fret_core::Paint::Solid(border_color).into(),
869 corner_radii: Corners::all(Px(point_r)),
870 });
871 }
872 }
873 _ => {}
874 }
875 }
876
877 let overlay_order = DrawOrder(style.draw_order.0.saturating_add(10_000));
878 let shadow_order = DrawOrder(style.draw_order.0.saturating_add(9_900));
879
880 if let Some(axis_pointer) = axis_pointer {
881 if let Some(rect) = axis_pointer.shadow_rect_px {
882 let color = Color {
883 a: 0.08,
884 ..style.selection_fill
885 };
886 painter.scene().push(fret_core::SceneOp::Quad {
887 order: shadow_order,
888 rect,
889 background: fret_core::Paint::Solid(color).into(),
890
891 border: Edges::all(Px(0.0)),
892 border_paint: fret_core::Paint::TRANSPARENT.into(),
893
894 corner_radii: Corners::all(Px(0.0)),
895 });
896 } else {
897 let plot = bounds;
898 let crosshair_w = style.crosshair_width.0.max(1.0);
899 let x = axis_pointer
900 .crosshair_px
901 .x
902 .0
903 .clamp(plot.origin.x.0, plot.origin.x.0 + plot.size.width.0);
904 let y = axis_pointer
905 .crosshair_px
906 .y
907 .0
908 .clamp(plot.origin.y.0, plot.origin.y.0 + plot.size.height.0);
909
910 if axis_pointer.draw_x {
911 painter.scene().push(fret_core::SceneOp::Quad {
912 order: overlay_order,
913 rect: Rect::new(
914 Point::new(Px(x - 0.5 * crosshair_w), plot.origin.y),
915 Size::new(Px(crosshair_w), plot.size.height),
916 ),
917 background: fret_core::Paint::Solid(style.crosshair_color).into(),
918
919 border: Edges::all(Px(0.0)),
920 border_paint: fret_core::Paint::TRANSPARENT.into(),
921
922 corner_radii: Corners::all(Px(0.0)),
923 });
924 }
925 if axis_pointer.draw_y {
926 painter.scene().push(fret_core::SceneOp::Quad {
927 order: overlay_order,
928 rect: Rect::new(
929 Point::new(plot.origin.x, Px(y - 0.5 * crosshair_w)),
930 Size::new(plot.size.width, Px(crosshair_w)),
931 ),
932 background: fret_core::Paint::Solid(style.crosshair_color).into(),
933
934 border: Edges::all(Px(0.0)),
935 border_paint: fret_core::Paint::TRANSPARENT.into(),
936
937 corner_radii: Corners::all(Px(0.0)),
938 });
939 }
940 }
941 }
942
943 if let Some(point) = hover_point_px {
944 let size = style.hover_point_size.0.max(1.0);
945 let r = 0.5 * size;
946 painter.scene().push(fret_core::SceneOp::Quad {
947 order: overlay_order,
948 rect: Rect::new(
949 Point::new(Px(point.x.0 - r), Px(point.y.0 - r)),
950 Size::new(Px(size), Px(size)),
951 ),
952 background: fret_core::Paint::Solid(style.hover_point_color).into(),
953
954 border: Edges::all(Px(0.0)),
955 border_paint: fret_core::Paint::TRANSPARENT.into(),
956
957 corner_radii: Corners::all(Px(r)),
958 });
959 }
960 });
961 };
962
963 let engine_k = engine.clone();
964 let legend_state_k = legend_state.clone();
965 let on_key_down: OnKeyDown = Arc::new(move |host, action_cx, down| {
966 let modifiers = down.modifiers;
967 let legend_mods_ok =
968 modifiers.ctrl && !modifiers.alt && !modifiers.alt_gr && !modifiers.meta;
969 if !legend_mods_ok {
970 return false;
971 }
972
973 let in_legend = legend_state_k
974 .lock()
975 .ok()
976 .is_some_and(|st| st.is_pointer_in_panel());
977 if !in_legend {
978 return false;
979 }
980
981 let changed = host
982 .models_mut()
983 .update(&engine_k, |engine| {
984 let model = engine.model();
985 let updates = match down.key {
986 KeyCode::KeyA if modifiers.shift => {
987 crate::legend_logic::legend_select_none_updates(model)
988 }
989 KeyCode::KeyA => crate::legend_logic::legend_select_all_updates(model),
990 KeyCode::KeyI if !modifiers.shift => {
991 crate::legend_logic::legend_invert_updates(model)
992 }
993 _ => return false,
994 };
995 if updates.is_empty() {
996 return false;
997 }
998 engine.apply_action(Action::SetSeriesVisibility { updates });
999 true
1000 })
1001 .ok()
1002 .unwrap_or(false);
1003
1004 if !changed {
1005 return false;
1006 }
1007 if let Ok(mut st) = legend_state_k.lock() {
1008 st.anchor = None;
1009 }
1010 host.request_redraw(action_cx.window);
1011 true
1012 });
1013
1014 let focus_props = FocusScopeProps::default();
1015 cx.focus_scope_with_id(focus_props, move |cx, focus_id| {
1016 cx.key_add_on_key_down_for(focus_id, on_key_down);
1017 vec![canvas_tool_router_panel(cx, router_props, tools, paint)]
1018 })
1019}