Skip to main content

sequoia_view/
canvas.rs

1#![forbid(unsafe_code)]
2
3use std::rc::Rc;
4
5use sequoia_core::{BlendMode, Color, Dirty, Fill, Length, Point, Rect, Size, TexInput, Vec2};
6use sequoia_input::{
7    CursorIcon, Event, EventPhase, ImeEvent, Key, KeyEvent, Modifiers, PointerButton, PointerEvent,
8};
9use sequoia_layout::LayoutStyle;
10
11use crate::painter::Painter;
12use crate::shader_pass::{ShaderChain, ShaderPass};
13use crate::view::{EventCtx, PaintCtx, View};
14
15#[derive(Clone)]
16pub struct KeyBinding {
17    pub key: Key,
18
19    pub modifiers: Modifiers,
20    action: Rc<dyn Fn()>,
21}
22
23impl KeyBinding {
24    pub fn new(key: Key, modifiers: Modifiers, action: impl Fn() + 'static) -> Self {
25        Self {
26            key,
27            modifiers,
28            action: Rc::new(action),
29        }
30    }
31
32    pub fn fire(&self) {
33        (self.action)();
34    }
35}
36
37#[derive(Clone, Copy, Debug, PartialEq)]
38pub struct Camera {
39    pub pos: Vec2,
40    pub zoom: f32,
41}
42
43impl Camera {
44    pub fn new(pos: Vec2, zoom: f32) -> Self {
45        let zoom = if zoom.is_finite() && zoom > 0.0 {
46            zoom
47        } else {
48            1.0
49        };
50        Self { pos, zoom }
51    }
52
53    pub fn identity() -> Self {
54        Self {
55            pos: Vec2::ZERO,
56            zoom: 1.0,
57        }
58    }
59}
60
61#[derive(Clone)]
62pub struct Canvas {
63    background: Option<Fill>,
64    focusable: bool,
65    capturable: bool,
66    cursor: CursorIcon,
67    style: LayoutStyle,
68    draw: Rc<dyn Fn(&mut Painter)>,
69    on_pointer: Option<Rc<dyn Fn(Point, &PointerEvent, &mut EventCtx)>>,
70    bindings: Vec<KeyBinding>,
71    on_key: Option<Rc<dyn Fn(&KeyEvent) -> bool>>,
72    on_ime: Option<Rc<dyn Fn(&ImeEvent) -> bool>>,
73    text_input: Option<Rc<dyn Fn() -> bool>>,
74    track: Option<Rc<dyn Fn()>>,
75    clip: bool,
76
77    overflow: f32,
78
79    grid: Option<(f32, Color)>,
80
81    axes: Option<Color>,
82
83    snap: Option<f32>,
84
85    camera: Option<Camera>,
86}
87
88impl Default for Canvas {
89    fn default() -> Self {
90        Self::new()
91    }
92}
93
94impl Canvas {
95    pub fn new() -> Self {
96        Self {
97            background: None,
98            focusable: true,
99            capturable: true,
100            cursor: CursorIcon::Default,
101            style: LayoutStyle {
102                width: Length::Fill(1.0),
103                height: Length::Fill(1.0),
104                ..LayoutStyle::default()
105            },
106            draw: Rc::new(|_| {}),
107            on_pointer: None,
108            bindings: Vec::new(),
109            on_key: None,
110            on_ime: None,
111            text_input: None,
112            track: None,
113            clip: true,
114            overflow: 0.0,
115            grid: None,
116            axes: None,
117            snap: None,
118            camera: None,
119        }
120    }
121
122    pub fn background(mut self, fill: impl Into<Fill>) -> Self {
123        self.background = Some(fill.into());
124        self
125    }
126
127    pub fn no_background(mut self) -> Self {
128        self.background = None;
129        self
130    }
131
132    pub fn focusable(mut self, focusable: bool) -> Self {
133        self.focusable = focusable;
134        self
135    }
136
137    pub fn capture(mut self, capturable: bool) -> Self {
138        self.capturable = capturable;
139        self
140    }
141
142    pub fn cursor(mut self, cursor: CursorIcon) -> Self {
143        self.cursor = cursor;
144        self
145    }
146
147    pub fn width(mut self, width: impl Into<Length>) -> Self {
148        self.style = self.style.width(width.into());
149        self
150    }
151
152    pub fn height(mut self, height: impl Into<Length>) -> Self {
153        self.style = self.style.height(height.into());
154        self
155    }
156
157    pub fn min_width(mut self, v: f32) -> Self {
158        self.style = self.style.min_width(v);
159        self
160    }
161
162    pub fn max_width(mut self, v: f32) -> Self {
163        self.style = self.style.max_width(v);
164        self
165    }
166
167    pub fn min_height(mut self, v: f32) -> Self {
168        self.style = self.style.min_height(v);
169        self
170    }
171
172    pub fn max_height(mut self, v: f32) -> Self {
173        self.style = self.style.max_height(v);
174        self
175    }
176
177    pub fn flex(mut self, weight: f32) -> Self {
178        self.style = self.style.flex(weight);
179        self
180    }
181
182    pub fn expand(self) -> Self {
183        self.flex(1.0)
184    }
185
186    pub fn fill_width(mut self) -> Self {
187        self.style = self.style.width(Length::Fill(1.0));
188        self
189    }
190
191    pub fn fill_height(mut self) -> Self {
192        self.style = self.style.height(Length::Fill(1.0));
193        self
194    }
195
196    pub fn fill_size(self) -> Self {
197        self.fill_width().fill_height()
198    }
199
200    pub fn on_draw(mut self, draw: impl Fn(&mut Painter) + 'static) -> Self {
201        self.draw = Rc::new(draw);
202        self
203    }
204
205    /// Replaces the canvas draw with `content` piped through `shader`.
206    ///
207    /// `content` renders into an offscreen target and `shader` is drawn over the
208    /// whole canvas with that target bound as its `content` input. The shader
209    /// must be registered (see `App::shader_type`).
210    #[must_use]
211    pub fn with_effect<E>(mut self, shader: E, content: impl Fn(&mut Painter) + 'static) -> Self
212    where
213        E: sequoia_shader::SequoiShader + Copy + 'static,
214    {
215        self.draw = Rc::new(move |g: &mut Painter| {
216            let rect = g.rect();
217            let (target, ()) = g.group(rect.size, sequoia_core::BlendMode::Normal, |inner| {
218                content(inner)
219            });
220            g.shader(
221                shader,
222                rect,
223                &[("content", sequoia_core::TexInput::Target(target))],
224            );
225        });
226        self
227    }
228
229    /// Replaces the canvas draw with a configured shader pass.
230    ///
231    /// The pass draws over the canvas bounds unless [`ShaderPass::at`] was
232    /// used. Missing registrations log a warning and skip the draw.
233    #[must_use]
234    pub fn with_shader_pass<S>(mut self, pass: ShaderPass<S>) -> Self
235    where
236        S: sequoia_shader::SequoiShader + Copy + 'static,
237    {
238        self.draw = Rc::new(move |g: &mut Painter| {
239            pass.draw(g);
240        });
241        self
242    }
243
244    /// Replaces the canvas draw with `content` piped through a configured pass.
245    ///
246    /// The pass's first declared texture is bound to the captured content unless
247    /// the pass already binds it explicitly. Use this instead of `with_effect`
248    /// when the effect has extra uniforms, a blend mode, or a UV window.
249    #[must_use]
250    pub fn with_effect_pass<S>(
251        mut self,
252        pass: ShaderPass<S>,
253        content: impl Fn(&mut Painter) + 'static,
254    ) -> Self
255    where
256        S: sequoia_shader::SequoiShader + Copy + 'static,
257    {
258        self.draw = Rc::new(move |g: &mut Painter| {
259            let rect = g.rect();
260            let (target, ()) = g.group(rect.size, BlendMode::Normal, |inner| content(inner));
261            pass.clone()
262                .at(rect)
263                .primary_input(TexInput::Target(target))
264                .draw(g);
265        });
266        self
267    }
268
269    /// Captures two layers and feeds them to the pass in declaration order.
270    ///
271    /// This is the convenient form of a transition or a two-input composite:
272    /// the first shader texture receives `first`, the second receives `second`.
273    #[must_use]
274    pub fn with_layer_effect<S>(
275        mut self,
276        pass: ShaderPass<S>,
277        first: impl Fn(&mut Painter) + 'static,
278        second: impl Fn(&mut Painter) + 'static,
279    ) -> Self
280    where
281        S: sequoia_shader::SequoiShader + Copy + 'static,
282    {
283        self.draw = Rc::new(move |g: &mut Painter| {
284            let rect = g.rect();
285            let (first_target, ()) = g.group(rect.size, BlendMode::Normal, |inner| first(inner));
286            let (second_target, ()) = g.group(rect.size, BlendMode::Normal, |inner| second(inner));
287            pass.clone()
288                .at(rect)
289                .layers(
290                    TexInput::Target(first_target),
291                    TexInput::Target(second_target),
292                )
293                .draw(g);
294        });
295        self
296    }
297
298    /// Pipes `content` through every pass in `chain`.
299    ///
300    /// Intermediate results live in offscreen targets; the final pass draws
301    /// into the canvas. A pass that is not registered is skipped and its input
302    /// is forwarded, so an optional effect does not blank the layer.
303    #[must_use]
304    pub fn with_effect_chain(
305        mut self,
306        content: impl Fn(&mut Painter) + 'static,
307        chain: ShaderChain,
308    ) -> Self {
309        self.draw = Rc::new(move |g: &mut Painter| {
310            if chain.is_empty() {
311                content(g);
312                return;
313            }
314            let rect = g.rect();
315            let (mut target, ()) = g.group(rect.size, BlendMode::Normal, |inner| content(inner));
316            let count = chain.len();
317            for (index, pass) in chain.passes().iter().enumerate() {
318                if index + 1 == count {
319                    if !(pass)(g, rect, TexInput::Target(target)) {
320                        g.blit(target, rect, [0.0, 0.0, 1.0, 1.0]);
321                    }
322                } else {
323                    let (next, drawn) = g.group(rect.size, BlendMode::Normal, |inner| {
324                        (pass)(inner, rect, TexInput::Target(target))
325                    });
326                    if drawn {
327                        target = next;
328                    }
329                }
330            }
331        });
332        self
333    }
334
335    /// Replaces the canvas draw with `content` masked by `mask`.
336    ///
337    /// Both closures render into their own offscreen target; the built-in
338    /// `apply_mask` shader keeps the layer color and multiplies its alpha by the
339    /// mask alpha.
340    #[must_use]
341    pub fn with_mask(
342        mut self,
343        mask: impl Fn(&mut Painter) + 'static,
344        content: impl Fn(&mut Painter) + 'static,
345    ) -> Self {
346        self.draw = Rc::new(move |g: &mut Painter| {
347            let rect = g.rect();
348            let (mask_target, ()) = g.group(rect.size, sequoia_core::BlendMode::Normal, |inner| {
349                mask(inner)
350            });
351            let (layer_target, ()) = g.group(rect.size, sequoia_core::BlendMode::Normal, |inner| {
352                content(inner)
353            });
354            g.shader(
355                sequoia_shader::ApplyMask,
356                rect,
357                &[
358                    ("mask", sequoia_core::TexInput::Target(mask_target)),
359                    ("layer", sequoia_core::TexInput::Target(layer_target)),
360                ],
361            );
362        });
363        self
364    }
365
366    pub fn on_pointer(
367        mut self,
368        handler: impl Fn(Point, &PointerEvent, &mut EventCtx) + 'static,
369    ) -> Self {
370        self.on_pointer = Some(Rc::new(handler));
371        self
372    }
373
374    pub fn key(mut self, key: Key, modifiers: Modifiers, action: impl Fn() + 'static) -> Self {
375        self.bindings.push(KeyBinding::new(key, modifiers, action));
376        self
377    }
378
379    pub fn on_key(mut self, handler: impl Fn(&KeyEvent) -> bool + 'static) -> Self {
380        self.on_key = Some(Rc::new(handler));
381        self
382    }
383
384    /// Forwards platform input-method events to `handler`.
385    ///
386    /// Return `true` from the handler when the event was consumed. Composition
387    /// events only reach the canvas while [`Canvas::text_input`] reports that it
388    /// is editing text.
389    pub fn on_ime(mut self, handler: impl Fn(&ImeEvent) -> bool + 'static) -> Self {
390        self.on_ime = Some(Rc::new(handler));
391        self
392    }
393
394    /// Reports this canvas as a text input while `editing` returns `true`.
395    ///
396    /// Hosts use this to keep plain-key accelerators from stealing keystrokes
397    /// and to enable the platform IME while an inline text editor is open on the
398    /// canvas. The predicate is read on every keystroke, so it can follow a
399    /// signal instead of the view being rebuilt.
400    pub fn text_input(mut self, editing: impl Fn() -> bool + 'static) -> Self {
401        self.text_input = Some(Rc::new(editing));
402        self
403    }
404
405    pub fn track(mut self, track: impl Fn() + 'static) -> Self {
406        self.track = Some(Rc::new(track));
407        self
408    }
409
410    pub fn clip(mut self, clip: bool) -> Self {
411        self.clip = clip;
412        self
413    }
414
415    pub fn allow_overflow(mut self, px: f32) -> Self {
416        self.overflow = if px.is_finite() { px.max(0.0) } else { 0.0 };
417        self
418    }
419
420    pub fn overflow_px(self, px: f32) -> Self {
421        self.allow_overflow(px)
422    }
423
424    pub fn paint_overflow(&self) -> f32 {
425        self.overflow
426    }
427
428    pub fn grid(mut self, step: f32) -> Self {
429        self.grid = Some((step, Color::rgba(0.5, 0.5, 0.5, 0.08)));
430        self
431    }
432
433    pub fn grid_colored(mut self, step: f32, color: Color) -> Self {
434        self.grid = Some((step, color));
435        self
436    }
437
438    pub fn axes(mut self) -> Self {
439        self.axes = Some(Color::rgba(0.5, 0.5, 0.5, 0.25));
440        self
441    }
442
443    pub fn axes_colored(mut self, color: Color) -> Self {
444        self.axes = Some(color);
445        self
446    }
447
448    pub fn snap_every(mut self, step: f32) -> Self {
449        self.snap = Some(step);
450        self
451    }
452
453    pub fn snap_step(&self) -> Option<f32> {
454        self.snap
455    }
456
457    pub fn snap_value(&self, value: f32) -> f32 {
458        match self.snap {
459            Some(step) => Self::snap(value, step),
460            None => value,
461        }
462    }
463
464    pub fn camera(mut self, pos: Vec2, zoom: f32) -> Self {
465        self.camera = Some(Camera::new(pos, zoom));
466        self
467    }
468
469    pub fn no_camera(mut self) -> Self {
470        self.camera = None;
471        self
472    }
473
474    pub fn camera_value(&self) -> Option<Camera> {
475        self.camera
476    }
477
478    pub fn background_fill(&self) -> Option<&Fill> {
479        self.background.as_ref()
480    }
481
482    pub fn is_focusable(&self) -> bool {
483        self.focusable
484    }
485
486    pub fn cursor_icon_value(&self) -> CursorIcon {
487        self.cursor
488    }
489
490    pub fn binding_count(&self) -> usize {
491        self.bindings.len()
492    }
493
494    pub fn world_to_local(world: Point, cam: Vec2, zoom: f32) -> Point {
495        let zoom = if zoom.is_finite() && zoom > 0.0 {
496            zoom
497        } else {
498            1.0
499        };
500        Point::new((world.x - cam.x) * zoom, (world.y - cam.y) * zoom)
501    }
502
503    pub fn local_to_world(local: Point, cam: Vec2, zoom: f32) -> Point {
504        let zoom = if zoom.is_finite() && zoom > 0.0 {
505            zoom
506        } else {
507            1.0
508        };
509        Point::new(cam.x + local.x / zoom, cam.y + local.y / zoom)
510    }
511
512    pub fn view_bounds(size: Size, cam: Vec2, zoom: f32) -> Rect {
513        let zoom = if zoom.is_finite() && zoom > 0.0 {
514            zoom
515        } else {
516            1.0
517        };
518        Rect::from_xywh(cam.x, cam.y, size.width / zoom, size.height / zoom)
519    }
520
521    pub fn fit_view(content: Rect, size: Size, padding: f32) -> (Vec2, f32) {
522        if size.width <= 0.0 || size.height <= 0.0 {
523            return (Vec2::new(content.left(), content.top()), 1.0);
524        }
525        let pad = padding.max(0.0);
526        let cw = (content.size.width + pad * 2.0).max(1.0);
527        let ch = (content.size.height + pad * 2.0).max(1.0);
528        let zoom = (size.width / cw).min(size.height / ch).clamp(0.01, 100.0);
529        let cx = content.left() + content.size.width * 0.5;
530        let cy = content.top() + content.size.height * 0.5;
531        let cam = Vec2::new(
532            cx - size.width / (2.0 * zoom),
533            cy - size.height / (2.0 * zoom),
534        );
535        (cam, zoom)
536    }
537
538    pub fn snap(value: f32, step: f32) -> f32 {
539        if !(step > 0.0) || !value.is_finite() {
540            return value;
541        }
542        (value / step).round() * step
543    }
544
545    pub fn snap_point(p: Point, step: f32) -> Point {
546        Point::new(Self::snap(p.x, step), Self::snap(p.y, step))
547    }
548
549    pub fn lod_banded(zoom: f32, breaks: &[f32]) -> usize {
550        if !(zoom > 0.0) {
551            return breaks.len();
552        }
553        for (i, b) in breaks.iter().enumerate() {
554            if zoom >= *b {
555                return i;
556            }
557        }
558        breaks.len()
559    }
560
561    pub fn show_detail(zoom: f32, world: f32, min_px: f32) -> bool {
562        if !(zoom > 0.0) || !(world > 0.0) || !(min_px > 0.0) {
563            return false;
564        }
565        world * zoom >= min_px
566    }
567
568    pub fn detail_alpha(zoom: f32, world: f32, min_px: f32, fade_px: f32) -> f32 {
569        if !(zoom > 0.0) || !(world > 0.0) || !(min_px > 0.0) {
570            return 0.0;
571        }
572        let px = world * zoom;
573        if px <= min_px {
574            return 0.0;
575        }
576        if !(fade_px > 0.0) {
577            return 1.0;
578        }
579        ((px - min_px) / fade_px).clamp(0.0, 1.0)
580    }
581}
582
583impl View for Canvas {
584    fn body(&self) -> impl View + 'static {
585        if let Some(track) = &self.track {
586            track();
587        }
588        crate::views::Unit
589    }
590
591    fn is_leaf(&self) -> bool {
592        true
593    }
594
595    fn focusable(&self) -> bool {
596        self.focusable
597    }
598
599    fn captures_pointer(&self) -> bool {
600        self.capturable
601    }
602
603    fn has_visual_state(&self) -> bool {
604        true
605    }
606
607    fn accepts_text_input(&self) -> bool {
608        self.text_input.as_ref().is_some_and(|editing| editing())
609    }
610
611    fn cursor_icon(&self, _state: sequoia_style::VisualState) -> sequoia_input::CursorIcon {
612        self.cursor
613    }
614
615    fn layout_style(&self) -> LayoutStyle {
616        self.style
617    }
618
619    fn signal_dirty(&self) -> Dirty {
620        Dirty::CANVAS
621    }
622
623    fn clip_self(&self) -> bool {
624        self.clip && self.overflow <= 0.0
625    }
626
627    fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
628        if ctx.phase != EventPhase::Target {
629            return;
630        }
631        match event {
632            Event::Pointer(ev) => {
633                if self.focusable
634                    && let PointerEvent::Down {
635                        button: PointerButton::Left,
636                        ..
637                    } = ev
638                {
639                    ctx.request_focus();
640                }
641                if let Some(handler) = &self.on_pointer {
642                    let screen = match *ev {
643                        PointerEvent::Down { pos, .. }
644                        | PointerEvent::Move { pos, .. }
645                        | PointerEvent::Up { pos, .. }
646                        | PointerEvent::Wheel { pos, .. }
647                        | PointerEvent::PenMove { pos, .. }
648                        | PointerEvent::PenDown { pos, .. }
649                        | PointerEvent::PenUp { pos, .. }
650                        | PointerEvent::TouchDown { pos, .. }
651                        | PointerEvent::TouchMove { pos, .. }
652                        | PointerEvent::TouchUp { pos, .. }
653                        | PointerEvent::Pinch { pos, .. } => pos,
654
655                        _ => ctx.pos,
656                    };
657
658                    let mut local =
659                        Point::new(screen.x - ctx.rect.left(), screen.y - ctx.rect.top());
660                    if let Some(step) = self.snap {
661                        local = Self::snap_point(local, step);
662                    }
663                    handler(local, ev, ctx);
664                }
665            }
666            Event::Key(key) => {
667                for b in &self.bindings {
668                    if b.key == key.key && b.modifiers == key.modifiers {
669                        b.fire();
670                        ctx.handled();
671                        return;
672                    }
673                }
674                if let Some(handler) = &self.on_key
675                    && handler(key)
676                {
677                    ctx.handled();
678                }
679            }
680            Event::Ime(ime) => {
681                if let Some(handler) = &self.on_ime
682                    && handler(ime)
683                {
684                    ctx.handled();
685                }
686            }
687            Event::FilesHovered { .. } | Event::FilesDropped { .. } | Event::FilesCancelled => {}
688
689            _ => {}
690        }
691    }
692
693    fn paint_self(&self, ctx: &mut PaintCtx) {
694        if self.clip && self.overflow > 0.0 && !ctx.rect.is_empty() {
695            ctx.push_clip(ctx.rect.inflate(self.overflow, self.overflow));
696        }
697        let mut g = Painter::new(ctx);
698        if let Some(bg) = &self.background {
699            let rect = g.rect();
700            g.fill(rect, bg.clone());
701        }
702
703        if let Some((step, color)) = self.grid {
704            match self.camera {
705                Some(cam) => {
706                    let s = step * cam.zoom;
707                    let off = Vec2::new(-cam.pos.x * cam.zoom, -cam.pos.y * cam.zoom);
708                    g.grid_offset(s, off, color);
709                }
710                None => {
711                    g.grid(step, color);
712                }
713            }
714        }
715        if let Some(color) = self.axes {
716            let origin = match self.camera {
717                Some(cam) => Self::world_to_local(Point::ZERO, cam.pos, cam.zoom),
718                None => Point::new(g.size().width * 0.5, g.size().height * 0.5),
719            };
720            g.axes(origin, color, 1.0);
721        }
722        (self.draw)(&mut g);
723    }
724}
725
726#[cfg(test)]
727mod tests {
728    use super::*;
729
730    fn vp(x: f32, y: f32, w: f32, h: f32) -> Rect {
731        Rect::from_xywh(x, y, w, h)
732    }
733
734    #[test]
735    fn fill_helpers_override_canvas_size() {
736        let c = Canvas::new()
737            .width(10.0)
738            .height(20.0)
739            .fill_width()
740            .fill_height();
741        let style = c.layout_style();
742        assert_eq!(style.width, Length::Fill(1.0));
743        assert_eq!(style.height, Length::Fill(1.0));
744    }
745
746    #[test]
747    fn overflow_disables_exact_clip() {
748        use crate::view::View;
749        assert_eq!(Canvas::new().paint_overflow(), 0.0);
750        assert!(Canvas::new().clip_self());
751        let c = Canvas::new().allow_overflow(7.0);
752        assert!((c.paint_overflow() - 7.0).abs() < 1e-6);
753        assert!(!c.clip_self(), "z overflow clip idzie w paint_self");
754        assert!(Canvas::new().allow_overflow(f32::NAN).clip_self());
755        assert!(Canvas::new().allow_overflow(-5.0).clip_self());
756        assert!(!Canvas::new().clip(false).clip_self());
757    }
758
759    #[test]
760    fn camera_roundtrip_local() {
761        let cam = Vec2::new(100.0, 200.0);
762        let world = Point::new(150.0, 260.0);
763        let local = Canvas::world_to_local(world, cam, 2.0);
764        assert_eq!(local, Point::new(100.0, 120.0));
765        assert_eq!(Canvas::local_to_world(local, cam, 2.0), world);
766        assert_eq!(
767            Canvas::local_to_world(local, cam, 0.0),
768            Point::new(cam.x + local.x, cam.y + local.y)
769        );
770    }
771
772    #[test]
773    fn view_bounds_matches_camera() {
774        let size = Size::new(200.0, 100.0);
775        let cam = Vec2::new(0.0, 0.0);
776        let world = Canvas::view_bounds(size, cam, 2.0);
777        assert_eq!(world.size, Size::new(100.0, 50.0));
778        assert!((world.left() - cam.x).abs() < 1e-4);
779        assert!((world.top() - cam.y).abs() < 1e-4);
780        let flat = Canvas::view_bounds(size, cam, 0.0);
781        assert!((flat.size.width - 200.0).abs() < 1e-4);
782    }
783
784    #[test]
785    fn fit_view_centers_content() {
786        let content = vp(0.0, 0.0, 400.0, 200.0);
787        let size = Size::new(800.0, 600.0);
788        let (cam, zoom) = Canvas::fit_view(content, size, 0.0);
789        assert_eq!(zoom, 2.0);
790        let center = Canvas::world_to_local(Point::new(200.0, 100.0), cam, zoom);
791        assert_eq!(center, Point::new(400.0, 300.0));
792        assert_eq!(
793            Canvas::fit_view(content, Size::ZERO, 0.0),
794            (Vec2::new(0.0, 0.0), 1.0)
795        );
796    }
797
798    #[test]
799    fn snap_rounds_to_grid() {
800        assert_eq!(Canvas::snap(13.0, 10.0), 10.0);
801        assert_eq!(Canvas::snap(17.0, 10.0), 20.0);
802        assert_eq!(Canvas::snap(-3.0, 10.0), 0.0);
803        assert_eq!(Canvas::snap(13.0, 0.0), 13.0);
804        assert_eq!(Canvas::snap(13.0, -5.0), 13.0);
805        let p = Canvas::snap_point(Point::new(13.0, 27.0), 10.0);
806        assert_eq!(p, Point::new(10.0, 30.0));
807    }
808
809    #[test]
810    fn lod_bands_from_full_to_coarse() {
811        let breaks = [1.0, 0.55, 0.3];
812        assert_eq!(Canvas::lod_banded(2.0, &breaks), 0);
813        assert_eq!(Canvas::lod_banded(1.0, &breaks), 0);
814        assert_eq!(Canvas::lod_banded(0.7, &breaks), 1);
815        assert_eq!(Canvas::lod_banded(0.55, &breaks), 1);
816        assert_eq!(Canvas::lod_banded(0.4, &breaks), 2);
817        assert_eq!(Canvas::lod_banded(0.1, &breaks), 3);
818        assert_eq!(Canvas::lod_banded(0.1, &[]), 0);
819        assert_eq!(Canvas::lod_banded(0.0, &breaks), 3);
820        assert_eq!(Canvas::lod_banded(-1.0, &breaks), 3);
821        assert_eq!(Canvas::lod_banded(f32::NAN, &breaks), 3);
822    }
823
824    #[test]
825    fn show_detail_hides_subpixel_stuff() {
826        assert!(Canvas::show_detail(1.0, 100.0, 6.0));
827        assert!(Canvas::show_detail(0.06, 100.0, 6.0));
828        assert!(!Canvas::show_detail(0.05, 100.0, 6.0));
829        assert!(!Canvas::show_detail(1.0, 100.0, 0.0));
830        assert!(!Canvas::show_detail(0.0, 100.0, 6.0));
831        assert!(!Canvas::show_detail(f32::NAN, 100.0, 6.0));
832    }
833
834    #[test]
835    fn detail_alpha_ramps_without_popping() {
836        assert_eq!(Canvas::detail_alpha(0.05, 100.0, 6.0, 4.0), 0.0);
837        assert_eq!(Canvas::detail_alpha(0.08, 100.0, 6.0, 4.0), 0.5);
838        assert_eq!(Canvas::detail_alpha(1.0, 100.0, 6.0, 4.0), 1.0);
839        assert_eq!(Canvas::detail_alpha(1.0, 100.0, 6.0, 0.0), 1.0);
840        assert_eq!(Canvas::detail_alpha(0.0, 100.0, 6.0, 4.0), 0.0);
841    }
842
843    #[test]
844    fn default_cursor_is_default() {
845        assert_eq!(
846            Canvas::new().cursor_icon_value(),
847            sequoia_input::CursorIcon::Default
848        );
849        assert_eq!(
850            Canvas::new()
851                .cursor(sequoia_input::CursorIcon::Crosshair)
852                .cursor_icon_value(),
853            sequoia_input::CursorIcon::Crosshair
854        );
855    }
856}