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