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 on_draw(mut self, draw: impl Fn(&mut Painter) + 'static) -> Self {
182        self.draw = Rc::new(draw);
183        self
184    }
185
186    pub fn on_pointer(
187        mut self,
188        handler: impl Fn(Point, &PointerEvent, &mut EventCtx) + 'static,
189    ) -> Self {
190        self.on_pointer = Some(Rc::new(handler));
191        self
192    }
193
194    pub fn key(mut self, key: Key, modifiers: Modifiers, action: impl Fn() + 'static) -> Self {
195        self.bindings.push(KeyBinding::new(key, modifiers, action));
196        self
197    }
198
199    pub fn on_key(mut self, handler: impl Fn(&KeyEvent) -> bool + 'static) -> Self {
200        self.on_key = Some(Rc::new(handler));
201        self
202    }
203
204    pub fn track(mut self, track: impl Fn() + 'static) -> Self {
205        self.track = Some(Rc::new(track));
206        self
207    }
208
209    pub fn clip(mut self, clip: bool) -> Self {
210        self.clip = clip;
211        self
212    }
213
214    pub fn allow_overflow(mut self, px: f32) -> Self {
215        self.overflow = if px.is_finite() { px.max(0.0) } else { 0.0 };
216        self
217    }
218
219    pub fn overflow_px(self, px: f32) -> Self {
220        self.allow_overflow(px)
221    }
222
223    pub fn paint_overflow(&self) -> f32 {
224        self.overflow
225    }
226
227    pub fn grid(mut self, step: f32) -> Self {
228        self.grid = Some((step, Color::rgba(0.5, 0.5, 0.5, 0.08)));
229        self
230    }
231
232    pub fn grid_colored(mut self, step: f32, color: Color) -> Self {
233        self.grid = Some((step, color));
234        self
235    }
236
237    pub fn axes(mut self) -> Self {
238        self.axes = Some(Color::rgba(0.5, 0.5, 0.5, 0.25));
239        self
240    }
241
242    pub fn axes_colored(mut self, color: Color) -> Self {
243        self.axes = Some(color);
244        self
245    }
246
247    pub fn snap_every(mut self, step: f32) -> Self {
248        self.snap = Some(step);
249        self
250    }
251
252    pub fn snap_step(&self) -> Option<f32> {
253        self.snap
254    }
255
256    pub fn snap_value(&self, value: f32) -> f32 {
257        match self.snap {
258            Some(step) => Self::snap(value, step),
259            None => value,
260        }
261    }
262
263    pub fn camera(mut self, pos: Vec2, zoom: f32) -> Self {
264        self.camera = Some(Camera::new(pos, zoom));
265        self
266    }
267
268    pub fn no_camera(mut self) -> Self {
269        self.camera = None;
270        self
271    }
272
273    pub fn camera_value(&self) -> Option<Camera> {
274        self.camera
275    }
276
277    pub fn background_fill(&self) -> Option<&Fill> {
278        self.background.as_ref()
279    }
280
281    pub fn is_focusable(&self) -> bool {
282        self.focusable
283    }
284
285    pub fn cursor_icon_value(&self) -> CursorIcon {
286        self.cursor
287    }
288
289    pub fn binding_count(&self) -> usize {
290        self.bindings.len()
291    }
292
293    pub fn world_to_local(world: Point, cam: Vec2, zoom: f32) -> Point {
294        let zoom = if zoom.is_finite() && zoom > 0.0 {
295            zoom
296        } else {
297            1.0
298        };
299        Point::new((world.x - cam.x) * zoom, (world.y - cam.y) * zoom)
300    }
301
302    pub fn local_to_world(local: Point, cam: Vec2, zoom: f32) -> Point {
303        let zoom = if zoom.is_finite() && zoom > 0.0 {
304            zoom
305        } else {
306            1.0
307        };
308        Point::new(cam.x + local.x / zoom, cam.y + local.y / zoom)
309    }
310
311    pub fn view_bounds(size: Size, cam: Vec2, zoom: f32) -> Rect {
312        let zoom = if zoom.is_finite() && zoom > 0.0 {
313            zoom
314        } else {
315            1.0
316        };
317        Rect::from_xywh(cam.x, cam.y, size.width / zoom, size.height / zoom)
318    }
319
320    pub fn fit_view(content: Rect, size: Size, padding: f32) -> (Vec2, f32) {
321        if size.width <= 0.0 || size.height <= 0.0 {
322            return (Vec2::new(content.left(), content.top()), 1.0);
323        }
324        let pad = padding.max(0.0);
325        let cw = (content.size.width + pad * 2.0).max(1.0);
326        let ch = (content.size.height + pad * 2.0).max(1.0);
327        let zoom = (size.width / cw).min(size.height / ch).clamp(0.01, 100.0);
328        let cx = content.left() + content.size.width * 0.5;
329        let cy = content.top() + content.size.height * 0.5;
330        let cam = Vec2::new(
331            cx - size.width / (2.0 * zoom),
332            cy - size.height / (2.0 * zoom),
333        );
334        (cam, zoom)
335    }
336
337    pub fn snap(value: f32, step: f32) -> f32 {
338        if !(step > 0.0) || !value.is_finite() {
339            return value;
340        }
341        (value / step).round() * step
342    }
343
344    pub fn snap_point(p: Point, step: f32) -> Point {
345        Point::new(Self::snap(p.x, step), Self::snap(p.y, step))
346    }
347
348    pub fn lod_banded(zoom: f32, breaks: &[f32]) -> usize {
349        if !(zoom > 0.0) {
350            return breaks.len();
351        }
352        for (i, b) in breaks.iter().enumerate() {
353            if zoom >= *b {
354                return i;
355            }
356        }
357        breaks.len()
358    }
359
360    pub fn show_detail(zoom: f32, world: f32, min_px: f32) -> bool {
361        if !(zoom > 0.0) || !(world > 0.0) || !(min_px > 0.0) {
362            return false;
363        }
364        world * zoom >= min_px
365    }
366
367    pub fn detail_alpha(zoom: f32, world: f32, min_px: f32, fade_px: f32) -> f32 {
368        if !(zoom > 0.0) || !(world > 0.0) || !(min_px > 0.0) {
369            return 0.0;
370        }
371        let px = world * zoom;
372        if px <= min_px {
373            return 0.0;
374        }
375        if !(fade_px > 0.0) {
376            return 1.0;
377        }
378        ((px - min_px) / fade_px).clamp(0.0, 1.0)
379    }
380}
381
382impl View for Canvas {
383    fn body(&self) -> impl View + 'static {
384        if let Some(track) = &self.track {
385            track();
386        }
387        crate::views::Unit
388    }
389
390    fn is_leaf(&self) -> bool {
391        true
392    }
393
394    fn focusable(&self) -> bool {
395        self.focusable
396    }
397
398    fn captures_pointer(&self) -> bool {
399        self.capturable
400    }
401
402    fn has_visual_state(&self) -> bool {
403        true
404    }
405
406    fn cursor_icon(&self, _state: sequoia_style::VisualState) -> sequoia_input::CursorIcon {
407        self.cursor
408    }
409
410    fn layout_style(&self) -> LayoutStyle {
411        self.style
412    }
413
414    fn clip_self(&self) -> bool {
415        self.clip && self.overflow <= 0.0
416    }
417
418    fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
419        if ctx.phase != EventPhase::Target {
420            return;
421        }
422        match event {
423            Event::Pointer(ev) => {
424                if self.focusable
425                    && let PointerEvent::Down {
426                        button: PointerButton::Left,
427                        ..
428                    } = ev
429                {
430                    ctx.request_focus();
431                }
432                if let Some(handler) = &self.on_pointer {
433                    let screen = match *ev {
434                        PointerEvent::Down { pos, .. }
435                        | PointerEvent::Move { pos, .. }
436                        | PointerEvent::Up { pos, .. }
437                        | PointerEvent::Wheel { pos, .. }
438                        | PointerEvent::PenMove { pos, .. }
439                        | PointerEvent::PenDown { pos, .. }
440                        | PointerEvent::PenUp { pos, .. }
441                        | PointerEvent::TouchDown { pos, .. }
442                        | PointerEvent::TouchMove { pos, .. }
443                        | PointerEvent::TouchUp { pos, .. }
444                        | PointerEvent::Pinch { pos, .. } => pos,
445
446                        _ => ctx.pos,
447                    };
448
449                    let mut local =
450                        Point::new(screen.x - ctx.rect.left(), screen.y - ctx.rect.top());
451                    if let Some(step) = self.snap {
452                        local = Self::snap_point(local, step);
453                    }
454                    handler(local, ev, ctx);
455                }
456            }
457            Event::Key(key) => {
458                for b in &self.bindings {
459                    if b.key == key.key && b.modifiers == key.modifiers {
460                        b.fire();
461                        ctx.handled();
462                        return;
463                    }
464                }
465                if let Some(handler) = &self.on_key
466                    && handler(key)
467                {
468                    ctx.handled();
469                }
470            }
471            Event::Ime(_) => {}
472            Event::FilesHovered { .. } | Event::FilesDropped { .. } | Event::FilesCancelled => {}
473
474            _ => {}
475        }
476    }
477
478    fn paint_self(&self, ctx: &mut PaintCtx) {
479        if self.clip && self.overflow > 0.0 && !ctx.rect.is_empty() {
480            ctx.push_clip(ctx.rect.inflate(self.overflow, self.overflow));
481        }
482        let mut g = Painter::new(ctx);
483        if let Some(bg) = &self.background {
484            let rect = g.rect();
485            g.fill(rect, bg.clone());
486        }
487
488        if let Some((step, color)) = self.grid {
489            match self.camera {
490                Some(cam) => {
491                    let s = step * cam.zoom;
492                    let off = Vec2::new(-cam.pos.x * cam.zoom, -cam.pos.y * cam.zoom);
493                    g.grid_offset(s, off, color);
494                }
495                None => {
496                    g.grid(step, color);
497                }
498            }
499        }
500        if let Some(color) = self.axes {
501            let origin = match self.camera {
502                Some(cam) => Self::world_to_local(Point::ZERO, cam.pos, cam.zoom),
503                None => Point::new(g.size().width * 0.5, g.size().height * 0.5),
504            };
505            g.axes(origin, color, 1.0);
506        }
507        (self.draw)(&mut g);
508    }
509}
510
511#[cfg(test)]
512mod tests {
513    use super::*;
514
515    fn vp(x: f32, y: f32, w: f32, h: f32) -> Rect {
516        Rect::from_xywh(x, y, w, h)
517    }
518
519    #[test]
520    fn overflow_disables_exact_clip() {
521        use crate::view::View;
522        assert_eq!(Canvas::new().paint_overflow(), 0.0);
523        assert!(Canvas::new().clip_self());
524        let c = Canvas::new().allow_overflow(7.0);
525        assert!((c.paint_overflow() - 7.0).abs() < 1e-6);
526        assert!(!c.clip_self(), "z overflow clip idzie w paint_self");
527        assert!(Canvas::new().allow_overflow(f32::NAN).clip_self());
528        assert!(Canvas::new().allow_overflow(-5.0).clip_self());
529        assert!(!Canvas::new().clip(false).clip_self());
530    }
531
532    #[test]
533    fn camera_roundtrip_local() {
534        let cam = Vec2::new(100.0, 200.0);
535        let world = Point::new(150.0, 260.0);
536        let local = Canvas::world_to_local(world, cam, 2.0);
537        assert_eq!(local, Point::new(100.0, 120.0));
538        assert_eq!(Canvas::local_to_world(local, cam, 2.0), world);
539        assert_eq!(
540            Canvas::local_to_world(local, cam, 0.0),
541            Point::new(cam.x + local.x, cam.y + local.y)
542        );
543    }
544
545    #[test]
546    fn view_bounds_matches_camera() {
547        let size = Size::new(200.0, 100.0);
548        let cam = Vec2::new(0.0, 0.0);
549        let world = Canvas::view_bounds(size, cam, 2.0);
550        assert_eq!(world.size, Size::new(100.0, 50.0));
551        assert!((world.left() - cam.x).abs() < 1e-4);
552        assert!((world.top() - cam.y).abs() < 1e-4);
553        let flat = Canvas::view_bounds(size, cam, 0.0);
554        assert!((flat.size.width - 200.0).abs() < 1e-4);
555    }
556
557    #[test]
558    fn fit_view_centers_content() {
559        let content = vp(0.0, 0.0, 400.0, 200.0);
560        let size = Size::new(800.0, 600.0);
561        let (cam, zoom) = Canvas::fit_view(content, size, 0.0);
562        assert_eq!(zoom, 2.0);
563        let center = Canvas::world_to_local(Point::new(200.0, 100.0), cam, zoom);
564        assert_eq!(center, Point::new(400.0, 300.0));
565        assert_eq!(
566            Canvas::fit_view(content, Size::ZERO, 0.0),
567            (Vec2::new(0.0, 0.0), 1.0)
568        );
569    }
570
571    #[test]
572    fn snap_rounds_to_grid() {
573        assert_eq!(Canvas::snap(13.0, 10.0), 10.0);
574        assert_eq!(Canvas::snap(17.0, 10.0), 20.0);
575        assert_eq!(Canvas::snap(-3.0, 10.0), 0.0);
576        assert_eq!(Canvas::snap(13.0, 0.0), 13.0);
577        assert_eq!(Canvas::snap(13.0, -5.0), 13.0);
578        let p = Canvas::snap_point(Point::new(13.0, 27.0), 10.0);
579        assert_eq!(p, Point::new(10.0, 30.0));
580    }
581
582    #[test]
583    fn lod_bands_from_full_to_coarse() {
584        let breaks = [1.0, 0.55, 0.3];
585        assert_eq!(Canvas::lod_banded(2.0, &breaks), 0);
586        assert_eq!(Canvas::lod_banded(1.0, &breaks), 0);
587        assert_eq!(Canvas::lod_banded(0.7, &breaks), 1);
588        assert_eq!(Canvas::lod_banded(0.55, &breaks), 1);
589        assert_eq!(Canvas::lod_banded(0.4, &breaks), 2);
590        assert_eq!(Canvas::lod_banded(0.1, &breaks), 3);
591        assert_eq!(Canvas::lod_banded(0.1, &[]), 0);
592        assert_eq!(Canvas::lod_banded(0.0, &breaks), 3);
593        assert_eq!(Canvas::lod_banded(-1.0, &breaks), 3);
594        assert_eq!(Canvas::lod_banded(f32::NAN, &breaks), 3);
595    }
596
597    #[test]
598    fn show_detail_hides_subpixel_stuff() {
599        assert!(Canvas::show_detail(1.0, 100.0, 6.0));
600        assert!(Canvas::show_detail(0.06, 100.0, 6.0));
601        assert!(!Canvas::show_detail(0.05, 100.0, 6.0));
602        assert!(!Canvas::show_detail(1.0, 100.0, 0.0));
603        assert!(!Canvas::show_detail(0.0, 100.0, 6.0));
604        assert!(!Canvas::show_detail(f32::NAN, 100.0, 6.0));
605    }
606
607    #[test]
608    fn detail_alpha_ramps_without_popping() {
609        assert_eq!(Canvas::detail_alpha(0.05, 100.0, 6.0, 4.0), 0.0);
610        assert_eq!(Canvas::detail_alpha(0.08, 100.0, 6.0, 4.0), 0.5);
611        assert_eq!(Canvas::detail_alpha(1.0, 100.0, 6.0, 4.0), 1.0);
612        assert_eq!(Canvas::detail_alpha(1.0, 100.0, 6.0, 0.0), 1.0);
613        assert_eq!(Canvas::detail_alpha(0.0, 100.0, 6.0, 4.0), 0.0);
614    }
615
616    #[test]
617    fn default_cursor_is_default() {
618        assert_eq!(
619            Canvas::new().cursor_icon_value(),
620            sequoia_input::CursorIcon::Default
621        );
622        assert_eq!(
623            Canvas::new()
624                .cursor(sequoia_input::CursorIcon::Crosshair)
625                .cursor_icon_value(),
626            sequoia_input::CursorIcon::Crosshair
627        );
628    }
629}