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 #[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 #[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 #[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 #[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 #[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 #[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 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 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}