Skip to main content

sequoia_view/
canvas.rs

1#![forbid(unsafe_code)]
2
3use std::rc::Rc;
4
5use sequoia_core::{Color, Fill, Length, Point, Rect, Size, Vec2};
6use sequoia_input::{
7    CursorIcon, Event, EventPhase, Key, KeyEvent, Modifiers, PointerButton, PointerEvent,
8};
9use sequoia_layout::LayoutStyle;
10
11use crate::painter::Painter;
12use crate::view::{EventCtx, PaintCtx, View};
13
14#[derive(Clone)]
15pub struct KeyBinding {
16    pub key: Key,
17
18    pub modifiers: Modifiers,
19    action: Rc<dyn Fn()>,
20}
21
22impl KeyBinding {
23    pub fn new(key: Key, modifiers: Modifiers, action: impl Fn() + 'static) -> Self {
24        Self {
25            key,
26            modifiers,
27            action: Rc::new(action),
28        }
29    }
30
31    pub fn fire(&self) {
32        (self.action)();
33    }
34}
35
36#[derive(Clone, Copy, Debug, PartialEq)]
37pub struct Camera {
38    pub pos: Vec2,
39    pub zoom: f32,
40}
41
42impl Camera {
43    pub fn new(pos: Vec2, zoom: f32) -> Self {
44        let zoom = if zoom.is_finite() && zoom > 0.0 {
45            zoom
46        } else {
47            1.0
48        };
49        Self { pos, zoom }
50    }
51
52    pub fn identity() -> Self {
53        Self {
54            pos: Vec2::ZERO,
55            zoom: 1.0,
56        }
57    }
58}
59
60#[derive(Clone)]
61pub struct Canvas {
62    background: Option<Fill>,
63    focusable: bool,
64    capturable: bool,
65    cursor: CursorIcon,
66    style: LayoutStyle,
67    draw: Rc<dyn Fn(&mut Painter)>,
68    on_pointer: Option<Rc<dyn Fn(Point, &PointerEvent, &mut EventCtx)>>,
69    bindings: Vec<KeyBinding>,
70    on_key: Option<Rc<dyn Fn(&KeyEvent) -> bool>>,
71    track: Option<Rc<dyn Fn()>>,
72    clip: bool,
73
74    overflow: f32,
75
76    grid: Option<(f32, Color)>,
77
78    axes: Option<Color>,
79
80    snap: Option<f32>,
81
82    camera: Option<Camera>,
83}
84
85impl Default for Canvas {
86    fn default() -> Self {
87        Self::new()
88    }
89}
90
91impl Canvas {
92    pub fn new() -> Self {
93        Self {
94            background: None,
95            focusable: true,
96            capturable: true,
97            cursor: CursorIcon::Default,
98            style: LayoutStyle {
99                width: Length::Fill(1.0),
100                height: Length::Fill(1.0),
101                ..LayoutStyle::default()
102            },
103            draw: Rc::new(|_| {}),
104            on_pointer: None,
105            bindings: Vec::new(),
106            on_key: None,
107            track: None,
108            clip: true,
109            overflow: 0.0,
110            grid: None,
111            axes: None,
112            snap: None,
113            camera: None,
114        }
115    }
116
117    pub fn background(mut self, fill: impl Into<Fill>) -> Self {
118        self.background = Some(fill.into());
119        self
120    }
121
122    pub fn no_background(mut self) -> Self {
123        self.background = None;
124        self
125    }
126
127    pub fn focusable(mut self, focusable: bool) -> Self {
128        self.focusable = focusable;
129        self
130    }
131
132    pub fn capture(mut self, capturable: bool) -> Self {
133        self.capturable = capturable;
134        self
135    }
136
137    pub fn cursor(mut self, cursor: CursorIcon) -> Self {
138        self.cursor = cursor;
139        self
140    }
141
142    pub fn width(mut self, width: impl Into<Length>) -> Self {
143        self.style = self.style.width(width.into());
144        self
145    }
146
147    pub fn height(mut self, height: impl Into<Length>) -> Self {
148        self.style = self.style.height(height.into());
149        self
150    }
151
152    pub fn min_width(mut self, v: f32) -> Self {
153        self.style = self.style.min_width(v);
154        self
155    }
156
157    pub fn max_width(mut self, v: f32) -> Self {
158        self.style = self.style.max_width(v);
159        self
160    }
161
162    pub fn min_height(mut self, v: f32) -> Self {
163        self.style = self.style.min_height(v);
164        self
165    }
166
167    pub fn max_height(mut self, v: f32) -> Self {
168        self.style = self.style.max_height(v);
169        self
170    }
171
172    pub fn flex(mut self, weight: f32) -> Self {
173        self.style = self.style.flex(weight);
174        self
175    }
176
177    pub fn expand(self) -> Self {
178        self.flex(1.0)
179    }
180
181    pub fn fill_width(mut self) -> Self {
182        self.style = self.style.width(Length::Fill(1.0));
183        self
184    }
185
186    pub fn fill_height(mut self) -> Self {
187        self.style = self.style.height(Length::Fill(1.0));
188        self
189    }
190
191    pub fn fill_size(self) -> Self {
192        self.fill_width().fill_height()
193    }
194
195    pub fn on_draw(mut self, draw: impl Fn(&mut Painter) + 'static) -> Self {
196        self.draw = Rc::new(draw);
197        self
198    }
199
200    pub fn on_pointer(
201        mut self,
202        handler: impl Fn(Point, &PointerEvent, &mut EventCtx) + 'static,
203    ) -> Self {
204        self.on_pointer = Some(Rc::new(handler));
205        self
206    }
207
208    pub fn key(mut self, key: Key, modifiers: Modifiers, action: impl Fn() + 'static) -> Self {
209        self.bindings.push(KeyBinding::new(key, modifiers, action));
210        self
211    }
212
213    pub fn on_key(mut self, handler: impl Fn(&KeyEvent) -> bool + 'static) -> Self {
214        self.on_key = Some(Rc::new(handler));
215        self
216    }
217
218    pub fn track(mut self, track: impl Fn() + 'static) -> Self {
219        self.track = Some(Rc::new(track));
220        self
221    }
222
223    pub fn clip(mut self, clip: bool) -> Self {
224        self.clip = clip;
225        self
226    }
227
228    pub fn allow_overflow(mut self, px: f32) -> Self {
229        self.overflow = if px.is_finite() { px.max(0.0) } else { 0.0 };
230        self
231    }
232
233    pub fn overflow_px(self, px: f32) -> Self {
234        self.allow_overflow(px)
235    }
236
237    pub fn paint_overflow(&self) -> f32 {
238        self.overflow
239    }
240
241    pub fn grid(mut self, step: f32) -> Self {
242        self.grid = Some((step, Color::rgba(0.5, 0.5, 0.5, 0.08)));
243        self
244    }
245
246    pub fn grid_colored(mut self, step: f32, color: Color) -> Self {
247        self.grid = Some((step, color));
248        self
249    }
250
251    pub fn axes(mut self) -> Self {
252        self.axes = Some(Color::rgba(0.5, 0.5, 0.5, 0.25));
253        self
254    }
255
256    pub fn axes_colored(mut self, color: Color) -> Self {
257        self.axes = Some(color);
258        self
259    }
260
261    pub fn snap_every(mut self, step: f32) -> Self {
262        self.snap = Some(step);
263        self
264    }
265
266    pub fn snap_step(&self) -> Option<f32> {
267        self.snap
268    }
269
270    pub fn snap_value(&self, value: f32) -> f32 {
271        match self.snap {
272            Some(step) => Self::snap(value, step),
273            None => value,
274        }
275    }
276
277    pub fn camera(mut self, pos: Vec2, zoom: f32) -> Self {
278        self.camera = Some(Camera::new(pos, zoom));
279        self
280    }
281
282    pub fn no_camera(mut self) -> Self {
283        self.camera = None;
284        self
285    }
286
287    pub fn camera_value(&self) -> Option<Camera> {
288        self.camera
289    }
290
291    pub fn background_fill(&self) -> Option<&Fill> {
292        self.background.as_ref()
293    }
294
295    pub fn is_focusable(&self) -> bool {
296        self.focusable
297    }
298
299    pub fn cursor_icon_value(&self) -> CursorIcon {
300        self.cursor
301    }
302
303    pub fn binding_count(&self) -> usize {
304        self.bindings.len()
305    }
306
307    pub fn world_to_local(world: Point, cam: Vec2, zoom: f32) -> Point {
308        let zoom = if zoom.is_finite() && zoom > 0.0 {
309            zoom
310        } else {
311            1.0
312        };
313        Point::new((world.x - cam.x) * zoom, (world.y - cam.y) * zoom)
314    }
315
316    pub fn local_to_world(local: Point, cam: Vec2, zoom: f32) -> Point {
317        let zoom = if zoom.is_finite() && zoom > 0.0 {
318            zoom
319        } else {
320            1.0
321        };
322        Point::new(cam.x + local.x / zoom, cam.y + local.y / zoom)
323    }
324
325    pub fn view_bounds(size: Size, cam: Vec2, zoom: f32) -> Rect {
326        let zoom = if zoom.is_finite() && zoom > 0.0 {
327            zoom
328        } else {
329            1.0
330        };
331        Rect::from_xywh(cam.x, cam.y, size.width / zoom, size.height / zoom)
332    }
333
334    pub fn fit_view(content: Rect, size: Size, padding: f32) -> (Vec2, f32) {
335        if size.width <= 0.0 || size.height <= 0.0 {
336            return (Vec2::new(content.left(), content.top()), 1.0);
337        }
338        let pad = padding.max(0.0);
339        let cw = (content.size.width + pad * 2.0).max(1.0);
340        let ch = (content.size.height + pad * 2.0).max(1.0);
341        let zoom = (size.width / cw).min(size.height / ch).clamp(0.01, 100.0);
342        let cx = content.left() + content.size.width * 0.5;
343        let cy = content.top() + content.size.height * 0.5;
344        let cam = Vec2::new(
345            cx - size.width / (2.0 * zoom),
346            cy - size.height / (2.0 * zoom),
347        );
348        (cam, zoom)
349    }
350
351    pub fn snap(value: f32, step: f32) -> f32 {
352        if !(step > 0.0) || !value.is_finite() {
353            return value;
354        }
355        (value / step).round() * step
356    }
357
358    pub fn snap_point(p: Point, step: f32) -> Point {
359        Point::new(Self::snap(p.x, step), Self::snap(p.y, step))
360    }
361
362    pub fn lod_banded(zoom: f32, breaks: &[f32]) -> usize {
363        if !(zoom > 0.0) {
364            return breaks.len();
365        }
366        for (i, b) in breaks.iter().enumerate() {
367            if zoom >= *b {
368                return i;
369            }
370        }
371        breaks.len()
372    }
373
374    pub fn show_detail(zoom: f32, world: f32, min_px: f32) -> bool {
375        if !(zoom > 0.0) || !(world > 0.0) || !(min_px > 0.0) {
376            return false;
377        }
378        world * zoom >= min_px
379    }
380
381    pub fn detail_alpha(zoom: f32, world: f32, min_px: f32, fade_px: f32) -> f32 {
382        if !(zoom > 0.0) || !(world > 0.0) || !(min_px > 0.0) {
383            return 0.0;
384        }
385        let px = world * zoom;
386        if px <= min_px {
387            return 0.0;
388        }
389        if !(fade_px > 0.0) {
390            return 1.0;
391        }
392        ((px - min_px) / fade_px).clamp(0.0, 1.0)
393    }
394}
395
396impl View for Canvas {
397    fn body(&self) -> impl View + 'static {
398        if let Some(track) = &self.track {
399            track();
400        }
401        crate::views::Unit
402    }
403
404    fn is_leaf(&self) -> bool {
405        true
406    }
407
408    fn focusable(&self) -> bool {
409        self.focusable
410    }
411
412    fn captures_pointer(&self) -> bool {
413        self.capturable
414    }
415
416    fn has_visual_state(&self) -> bool {
417        true
418    }
419
420    fn cursor_icon(&self, _state: sequoia_style::VisualState) -> sequoia_input::CursorIcon {
421        self.cursor
422    }
423
424    fn layout_style(&self) -> LayoutStyle {
425        self.style
426    }
427
428    fn clip_self(&self) -> bool {
429        self.clip && self.overflow <= 0.0
430    }
431
432    fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
433        if ctx.phase != EventPhase::Target {
434            return;
435        }
436        match event {
437            Event::Pointer(ev) => {
438                if self.focusable
439                    && let PointerEvent::Down {
440                        button: PointerButton::Left,
441                        ..
442                    } = ev
443                {
444                    ctx.request_focus();
445                }
446                if let Some(handler) = &self.on_pointer {
447                    let screen = match *ev {
448                        PointerEvent::Down { pos, .. }
449                        | PointerEvent::Move { pos, .. }
450                        | PointerEvent::Up { pos, .. }
451                        | PointerEvent::Wheel { pos, .. }
452                        | PointerEvent::PenMove { pos, .. }
453                        | PointerEvent::PenDown { pos, .. }
454                        | PointerEvent::PenUp { pos, .. }
455                        | PointerEvent::TouchDown { pos, .. }
456                        | PointerEvent::TouchMove { pos, .. }
457                        | PointerEvent::TouchUp { pos, .. }
458                        | PointerEvent::Pinch { pos, .. } => pos,
459
460                        _ => ctx.pos,
461                    };
462
463                    let mut local =
464                        Point::new(screen.x - ctx.rect.left(), screen.y - ctx.rect.top());
465                    if let Some(step) = self.snap {
466                        local = Self::snap_point(local, step);
467                    }
468                    handler(local, ev, ctx);
469                }
470            }
471            Event::Key(key) => {
472                for b in &self.bindings {
473                    if b.key == key.key && b.modifiers == key.modifiers {
474                        b.fire();
475                        ctx.handled();
476                        return;
477                    }
478                }
479                if let Some(handler) = &self.on_key
480                    && handler(key)
481                {
482                    ctx.handled();
483                }
484            }
485            Event::Ime(_) => {}
486            Event::FilesHovered { .. } | Event::FilesDropped { .. } | Event::FilesCancelled => {}
487
488            _ => {}
489        }
490    }
491
492    fn paint_self(&self, ctx: &mut PaintCtx) {
493        if self.clip && self.overflow > 0.0 && !ctx.rect.is_empty() {
494            ctx.push_clip(ctx.rect.inflate(self.overflow, self.overflow));
495        }
496        let mut g = Painter::new(ctx);
497        if let Some(bg) = &self.background {
498            let rect = g.rect();
499            g.fill(rect, bg.clone());
500        }
501
502        if let Some((step, color)) = self.grid {
503            match self.camera {
504                Some(cam) => {
505                    let s = step * cam.zoom;
506                    let off = Vec2::new(-cam.pos.x * cam.zoom, -cam.pos.y * cam.zoom);
507                    g.grid_offset(s, off, color);
508                }
509                None => {
510                    g.grid(step, color);
511                }
512            }
513        }
514        if let Some(color) = self.axes {
515            let origin = match self.camera {
516                Some(cam) => Self::world_to_local(Point::ZERO, cam.pos, cam.zoom),
517                None => Point::new(g.size().width * 0.5, g.size().height * 0.5),
518            };
519            g.axes(origin, color, 1.0);
520        }
521        (self.draw)(&mut g);
522    }
523}
524
525#[cfg(test)]
526mod tests {
527    use super::*;
528
529    fn vp(x: f32, y: f32, w: f32, h: f32) -> Rect {
530        Rect::from_xywh(x, y, w, h)
531    }
532
533    #[test]
534    fn fill_helpers_override_canvas_size() {
535        let c = Canvas::new()
536            .width(10.0)
537            .height(20.0)
538            .fill_width()
539            .fill_height();
540        let style = c.layout_style();
541        assert_eq!(style.width, Length::Fill(1.0));
542        assert_eq!(style.height, Length::Fill(1.0));
543    }
544
545    #[test]
546    fn overflow_disables_exact_clip() {
547        use crate::view::View;
548        assert_eq!(Canvas::new().paint_overflow(), 0.0);
549        assert!(Canvas::new().clip_self());
550        let c = Canvas::new().allow_overflow(7.0);
551        assert!((c.paint_overflow() - 7.0).abs() < 1e-6);
552        assert!(!c.clip_self(), "z overflow clip idzie w paint_self");
553        assert!(Canvas::new().allow_overflow(f32::NAN).clip_self());
554        assert!(Canvas::new().allow_overflow(-5.0).clip_self());
555        assert!(!Canvas::new().clip(false).clip_self());
556    }
557
558    #[test]
559    fn camera_roundtrip_local() {
560        let cam = Vec2::new(100.0, 200.0);
561        let world = Point::new(150.0, 260.0);
562        let local = Canvas::world_to_local(world, cam, 2.0);
563        assert_eq!(local, Point::new(100.0, 120.0));
564        assert_eq!(Canvas::local_to_world(local, cam, 2.0), world);
565        assert_eq!(
566            Canvas::local_to_world(local, cam, 0.0),
567            Point::new(cam.x + local.x, cam.y + local.y)
568        );
569    }
570
571    #[test]
572    fn view_bounds_matches_camera() {
573        let size = Size::new(200.0, 100.0);
574        let cam = Vec2::new(0.0, 0.0);
575        let world = Canvas::view_bounds(size, cam, 2.0);
576        assert_eq!(world.size, Size::new(100.0, 50.0));
577        assert!((world.left() - cam.x).abs() < 1e-4);
578        assert!((world.top() - cam.y).abs() < 1e-4);
579        let flat = Canvas::view_bounds(size, cam, 0.0);
580        assert!((flat.size.width - 200.0).abs() < 1e-4);
581    }
582
583    #[test]
584    fn fit_view_centers_content() {
585        let content = vp(0.0, 0.0, 400.0, 200.0);
586        let size = Size::new(800.0, 600.0);
587        let (cam, zoom) = Canvas::fit_view(content, size, 0.0);
588        assert_eq!(zoom, 2.0);
589        let center = Canvas::world_to_local(Point::new(200.0, 100.0), cam, zoom);
590        assert_eq!(center, Point::new(400.0, 300.0));
591        assert_eq!(
592            Canvas::fit_view(content, Size::ZERO, 0.0),
593            (Vec2::new(0.0, 0.0), 1.0)
594        );
595    }
596
597    #[test]
598    fn snap_rounds_to_grid() {
599        assert_eq!(Canvas::snap(13.0, 10.0), 10.0);
600        assert_eq!(Canvas::snap(17.0, 10.0), 20.0);
601        assert_eq!(Canvas::snap(-3.0, 10.0), 0.0);
602        assert_eq!(Canvas::snap(13.0, 0.0), 13.0);
603        assert_eq!(Canvas::snap(13.0, -5.0), 13.0);
604        let p = Canvas::snap_point(Point::new(13.0, 27.0), 10.0);
605        assert_eq!(p, Point::new(10.0, 30.0));
606    }
607
608    #[test]
609    fn lod_bands_from_full_to_coarse() {
610        let breaks = [1.0, 0.55, 0.3];
611        assert_eq!(Canvas::lod_banded(2.0, &breaks), 0);
612        assert_eq!(Canvas::lod_banded(1.0, &breaks), 0);
613        assert_eq!(Canvas::lod_banded(0.7, &breaks), 1);
614        assert_eq!(Canvas::lod_banded(0.55, &breaks), 1);
615        assert_eq!(Canvas::lod_banded(0.4, &breaks), 2);
616        assert_eq!(Canvas::lod_banded(0.1, &breaks), 3);
617        assert_eq!(Canvas::lod_banded(0.1, &[]), 0);
618        assert_eq!(Canvas::lod_banded(0.0, &breaks), 3);
619        assert_eq!(Canvas::lod_banded(-1.0, &breaks), 3);
620        assert_eq!(Canvas::lod_banded(f32::NAN, &breaks), 3);
621    }
622
623    #[test]
624    fn show_detail_hides_subpixel_stuff() {
625        assert!(Canvas::show_detail(1.0, 100.0, 6.0));
626        assert!(Canvas::show_detail(0.06, 100.0, 6.0));
627        assert!(!Canvas::show_detail(0.05, 100.0, 6.0));
628        assert!(!Canvas::show_detail(1.0, 100.0, 0.0));
629        assert!(!Canvas::show_detail(0.0, 100.0, 6.0));
630        assert!(!Canvas::show_detail(f32::NAN, 100.0, 6.0));
631    }
632
633    #[test]
634    fn detail_alpha_ramps_without_popping() {
635        assert_eq!(Canvas::detail_alpha(0.05, 100.0, 6.0, 4.0), 0.0);
636        assert_eq!(Canvas::detail_alpha(0.08, 100.0, 6.0, 4.0), 0.5);
637        assert_eq!(Canvas::detail_alpha(1.0, 100.0, 6.0, 4.0), 1.0);
638        assert_eq!(Canvas::detail_alpha(1.0, 100.0, 6.0, 0.0), 1.0);
639        assert_eq!(Canvas::detail_alpha(0.0, 100.0, 6.0, 4.0), 0.0);
640    }
641
642    #[test]
643    fn default_cursor_is_default() {
644        assert_eq!(
645            Canvas::new().cursor_icon_value(),
646            sequoia_input::CursorIcon::Default
647        );
648        assert_eq!(
649            Canvas::new()
650                .cursor(sequoia_input::CursorIcon::Crosshair)
651                .cursor_icon_value(),
652            sequoia_input::CursorIcon::Crosshair
653        );
654    }
655}