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