1#![forbid(unsafe_code)]
2
3use std::rc::Rc;
4
5use sequoia_core::{Color, Fill, Length, Point, Rect, Size, Vec2};
6use sequoia_input::{
7 CursorIcon, Event, EventPhase, Key, KeyEvent, Modifiers, PointerButton, PointerEvent,
8};
9use sequoia_layout::LayoutStyle;
10
11use crate::painter::Painter;
12use crate::view::{EventCtx, PaintCtx, View};
13
14#[derive(Clone)]
15pub struct KeyBinding {
16 pub key: Key,
17
18 pub modifiers: Modifiers,
19 action: Rc<dyn Fn()>,
20}
21
22impl KeyBinding {
23 pub fn new(key: Key, modifiers: Modifiers, action: impl Fn() + 'static) -> Self {
24 Self {
25 key,
26 modifiers,
27 action: Rc::new(action),
28 }
29 }
30
31 pub fn fire(&self) {
32 (self.action)();
33 }
34}
35
36#[derive(Clone, Copy, Debug, PartialEq)]
37pub struct Camera {
38 pub pos: Vec2,
39 pub zoom: f32,
40}
41
42impl Camera {
43 pub fn new(pos: Vec2, zoom: f32) -> Self {
44 let zoom = if zoom.is_finite() && zoom > 0.0 {
45 zoom
46 } else {
47 1.0
48 };
49 Self { pos, zoom }
50 }
51
52 pub fn identity() -> Self {
53 Self {
54 pos: Vec2::ZERO,
55 zoom: 1.0,
56 }
57 }
58}
59
60#[derive(Clone)]
61pub struct Canvas {
62 background: Option<Fill>,
63 focusable: bool,
64 capturable: bool,
65 cursor: CursorIcon,
66 style: LayoutStyle,
67 draw: Rc<dyn Fn(&mut Painter)>,
68 on_pointer: Option<Rc<dyn Fn(Point, &PointerEvent, &mut EventCtx)>>,
69 bindings: Vec<KeyBinding>,
70 on_key: Option<Rc<dyn Fn(&KeyEvent) -> bool>>,
71 track: Option<Rc<dyn Fn()>>,
72 clip: bool,
73
74 overflow: f32,
75
76 grid: Option<(f32, Color)>,
77
78 axes: Option<Color>,
79
80 snap: Option<f32>,
81
82 camera: Option<Camera>,
83}
84
85impl Default for Canvas {
86 fn default() -> Self {
87 Self::new()
88 }
89}
90
91impl Canvas {
92 pub fn new() -> Self {
93 Self {
94 background: None,
95 focusable: true,
96 capturable: true,
97 cursor: CursorIcon::Default,
98 style: LayoutStyle {
99 width: Length::Fill(1.0),
100 height: Length::Fill(1.0),
101 ..LayoutStyle::default()
102 },
103 draw: Rc::new(|_| {}),
104 on_pointer: None,
105 bindings: Vec::new(),
106 on_key: None,
107 track: None,
108 clip: true,
109 overflow: 0.0,
110 grid: None,
111 axes: None,
112 snap: None,
113 camera: None,
114 }
115 }
116
117 pub fn background(mut self, fill: impl Into<Fill>) -> Self {
118 self.background = Some(fill.into());
119 self
120 }
121
122 pub fn no_background(mut self) -> Self {
123 self.background = None;
124 self
125 }
126
127 pub fn focusable(mut self, focusable: bool) -> Self {
128 self.focusable = focusable;
129 self
130 }
131
132 pub fn capture(mut self, capturable: bool) -> Self {
133 self.capturable = capturable;
134 self
135 }
136
137 pub fn cursor(mut self, cursor: CursorIcon) -> Self {
138 self.cursor = cursor;
139 self
140 }
141
142 pub fn width(mut self, width: impl Into<Length>) -> Self {
143 self.style = self.style.width(width.into());
144 self
145 }
146
147 pub fn height(mut self, height: impl Into<Length>) -> Self {
148 self.style = self.style.height(height.into());
149 self
150 }
151
152 pub fn min_width(mut self, v: f32) -> Self {
153 self.style = self.style.min_width(v);
154 self
155 }
156
157 pub fn max_width(mut self, v: f32) -> Self {
158 self.style = self.style.max_width(v);
159 self
160 }
161
162 pub fn min_height(mut self, v: f32) -> Self {
163 self.style = self.style.min_height(v);
164 self
165 }
166
167 pub fn max_height(mut self, v: f32) -> Self {
168 self.style = self.style.max_height(v);
169 self
170 }
171
172 pub fn flex(mut self, weight: f32) -> Self {
173 self.style = self.style.flex(weight);
174 self
175 }
176
177 pub fn expand(self) -> Self {
178 self.flex(1.0)
179 }
180
181 pub fn fill_width(mut self) -> Self {
182 self.style = self.style.width(Length::Fill(1.0));
183 self
184 }
185
186 pub fn fill_height(mut self) -> Self {
187 self.style = self.style.height(Length::Fill(1.0));
188 self
189 }
190
191 pub fn fill_size(self) -> Self {
192 self.fill_width().fill_height()
193 }
194
195 pub fn on_draw(mut self, draw: impl Fn(&mut Painter) + 'static) -> Self {
196 self.draw = Rc::new(draw);
197 self
198 }
199
200 pub fn on_pointer(
201 mut self,
202 handler: impl Fn(Point, &PointerEvent, &mut EventCtx) + 'static,
203 ) -> Self {
204 self.on_pointer = Some(Rc::new(handler));
205 self
206 }
207
208 pub fn key(mut self, key: Key, modifiers: Modifiers, action: impl Fn() + 'static) -> Self {
209 self.bindings.push(KeyBinding::new(key, modifiers, action));
210 self
211 }
212
213 pub fn on_key(mut self, handler: impl Fn(&KeyEvent) -> bool + 'static) -> Self {
214 self.on_key = Some(Rc::new(handler));
215 self
216 }
217
218 pub fn track(mut self, track: impl Fn() + 'static) -> Self {
219 self.track = Some(Rc::new(track));
220 self
221 }
222
223 pub fn clip(mut self, clip: bool) -> Self {
224 self.clip = clip;
225 self
226 }
227
228 pub fn allow_overflow(mut self, px: f32) -> Self {
229 self.overflow = if px.is_finite() { px.max(0.0) } else { 0.0 };
230 self
231 }
232
233 pub fn overflow_px(self, px: f32) -> Self {
234 self.allow_overflow(px)
235 }
236
237 pub fn paint_overflow(&self) -> f32 {
238 self.overflow
239 }
240
241 pub fn grid(mut self, step: f32) -> Self {
242 self.grid = Some((step, Color::rgba(0.5, 0.5, 0.5, 0.08)));
243 self
244 }
245
246 pub fn grid_colored(mut self, step: f32, color: Color) -> Self {
247 self.grid = Some((step, color));
248 self
249 }
250
251 pub fn axes(mut self) -> Self {
252 self.axes = Some(Color::rgba(0.5, 0.5, 0.5, 0.25));
253 self
254 }
255
256 pub fn axes_colored(mut self, color: Color) -> Self {
257 self.axes = Some(color);
258 self
259 }
260
261 pub fn snap_every(mut self, step: f32) -> Self {
262 self.snap = Some(step);
263 self
264 }
265
266 pub fn snap_step(&self) -> Option<f32> {
267 self.snap
268 }
269
270 pub fn snap_value(&self, value: f32) -> f32 {
271 match self.snap {
272 Some(step) => Self::snap(value, step),
273 None => value,
274 }
275 }
276
277 pub fn camera(mut self, pos: Vec2, zoom: f32) -> Self {
278 self.camera = Some(Camera::new(pos, zoom));
279 self
280 }
281
282 pub fn no_camera(mut self) -> Self {
283 self.camera = None;
284 self
285 }
286
287 pub fn camera_value(&self) -> Option<Camera> {
288 self.camera
289 }
290
291 pub fn background_fill(&self) -> Option<&Fill> {
292 self.background.as_ref()
293 }
294
295 pub fn is_focusable(&self) -> bool {
296 self.focusable
297 }
298
299 pub fn cursor_icon_value(&self) -> CursorIcon {
300 self.cursor
301 }
302
303 pub fn binding_count(&self) -> usize {
304 self.bindings.len()
305 }
306
307 pub fn world_to_local(world: Point, cam: Vec2, zoom: f32) -> Point {
308 let zoom = if zoom.is_finite() && zoom > 0.0 {
309 zoom
310 } else {
311 1.0
312 };
313 Point::new((world.x - cam.x) * zoom, (world.y - cam.y) * zoom)
314 }
315
316 pub fn local_to_world(local: Point, cam: Vec2, zoom: f32) -> Point {
317 let zoom = if zoom.is_finite() && zoom > 0.0 {
318 zoom
319 } else {
320 1.0
321 };
322 Point::new(cam.x + local.x / zoom, cam.y + local.y / zoom)
323 }
324
325 pub fn view_bounds(size: Size, cam: Vec2, zoom: f32) -> Rect {
326 let zoom = if zoom.is_finite() && zoom > 0.0 {
327 zoom
328 } else {
329 1.0
330 };
331 Rect::from_xywh(cam.x, cam.y, size.width / zoom, size.height / zoom)
332 }
333
334 pub fn fit_view(content: Rect, size: Size, padding: f32) -> (Vec2, f32) {
335 if size.width <= 0.0 || size.height <= 0.0 {
336 return (Vec2::new(content.left(), content.top()), 1.0);
337 }
338 let pad = padding.max(0.0);
339 let cw = (content.size.width + pad * 2.0).max(1.0);
340 let ch = (content.size.height + pad * 2.0).max(1.0);
341 let zoom = (size.width / cw).min(size.height / ch).clamp(0.01, 100.0);
342 let cx = content.left() + content.size.width * 0.5;
343 let cy = content.top() + content.size.height * 0.5;
344 let cam = Vec2::new(
345 cx - size.width / (2.0 * zoom),
346 cy - size.height / (2.0 * zoom),
347 );
348 (cam, zoom)
349 }
350
351 pub fn snap(value: f32, step: f32) -> f32 {
352 if !(step > 0.0) || !value.is_finite() {
353 return value;
354 }
355 (value / step).round() * step
356 }
357
358 pub fn snap_point(p: Point, step: f32) -> Point {
359 Point::new(Self::snap(p.x, step), Self::snap(p.y, step))
360 }
361
362 pub fn lod_banded(zoom: f32, breaks: &[f32]) -> usize {
363 if !(zoom > 0.0) {
364 return breaks.len();
365 }
366 for (i, b) in breaks.iter().enumerate() {
367 if zoom >= *b {
368 return i;
369 }
370 }
371 breaks.len()
372 }
373
374 pub fn show_detail(zoom: f32, world: f32, min_px: f32) -> bool {
375 if !(zoom > 0.0) || !(world > 0.0) || !(min_px > 0.0) {
376 return false;
377 }
378 world * zoom >= min_px
379 }
380
381 pub fn detail_alpha(zoom: f32, world: f32, min_px: f32, fade_px: f32) -> f32 {
382 if !(zoom > 0.0) || !(world > 0.0) || !(min_px > 0.0) {
383 return 0.0;
384 }
385 let px = world * zoom;
386 if px <= min_px {
387 return 0.0;
388 }
389 if !(fade_px > 0.0) {
390 return 1.0;
391 }
392 ((px - min_px) / fade_px).clamp(0.0, 1.0)
393 }
394}
395
396impl View for Canvas {
397 fn body(&self) -> impl View + 'static {
398 if let Some(track) = &self.track {
399 track();
400 }
401 crate::views::Unit
402 }
403
404 fn is_leaf(&self) -> bool {
405 true
406 }
407
408 fn focusable(&self) -> bool {
409 self.focusable
410 }
411
412 fn captures_pointer(&self) -> bool {
413 self.capturable
414 }
415
416 fn has_visual_state(&self) -> bool {
417 true
418 }
419
420 fn cursor_icon(&self, _state: sequoia_style::VisualState) -> sequoia_input::CursorIcon {
421 self.cursor
422 }
423
424 fn layout_style(&self) -> LayoutStyle {
425 self.style
426 }
427
428 fn clip_self(&self) -> bool {
429 self.clip && self.overflow <= 0.0
430 }
431
432 fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
433 if ctx.phase != EventPhase::Target {
434 return;
435 }
436 match event {
437 Event::Pointer(ev) => {
438 if self.focusable
439 && let PointerEvent::Down {
440 button: PointerButton::Left,
441 ..
442 } = ev
443 {
444 ctx.request_focus();
445 }
446 if let Some(handler) = &self.on_pointer {
447 let screen = match *ev {
448 PointerEvent::Down { pos, .. }
449 | PointerEvent::Move { pos, .. }
450 | PointerEvent::Up { pos, .. }
451 | PointerEvent::Wheel { pos, .. }
452 | PointerEvent::PenMove { pos, .. }
453 | PointerEvent::PenDown { pos, .. }
454 | PointerEvent::PenUp { pos, .. }
455 | PointerEvent::TouchDown { pos, .. }
456 | PointerEvent::TouchMove { pos, .. }
457 | PointerEvent::TouchUp { pos, .. }
458 | PointerEvent::Pinch { pos, .. } => pos,
459
460 _ => ctx.pos,
461 };
462
463 let mut local =
464 Point::new(screen.x - ctx.rect.left(), screen.y - ctx.rect.top());
465 if let Some(step) = self.snap {
466 local = Self::snap_point(local, step);
467 }
468 handler(local, ev, ctx);
469 }
470 }
471 Event::Key(key) => {
472 for b in &self.bindings {
473 if b.key == key.key && b.modifiers == key.modifiers {
474 b.fire();
475 ctx.handled();
476 return;
477 }
478 }
479 if let Some(handler) = &self.on_key
480 && handler(key)
481 {
482 ctx.handled();
483 }
484 }
485 Event::Ime(_) => {}
486 Event::FilesHovered { .. } | Event::FilesDropped { .. } | Event::FilesCancelled => {}
487
488 _ => {}
489 }
490 }
491
492 fn paint_self(&self, ctx: &mut PaintCtx) {
493 if self.clip && self.overflow > 0.0 && !ctx.rect.is_empty() {
494 ctx.push_clip(ctx.rect.inflate(self.overflow, self.overflow));
495 }
496 let mut g = Painter::new(ctx);
497 if let Some(bg) = &self.background {
498 let rect = g.rect();
499 g.fill(rect, bg.clone());
500 }
501
502 if let Some((step, color)) = self.grid {
503 match self.camera {
504 Some(cam) => {
505 let s = step * cam.zoom;
506 let off = Vec2::new(-cam.pos.x * cam.zoom, -cam.pos.y * cam.zoom);
507 g.grid_offset(s, off, color);
508 }
509 None => {
510 g.grid(step, color);
511 }
512 }
513 }
514 if let Some(color) = self.axes {
515 let origin = match self.camera {
516 Some(cam) => Self::world_to_local(Point::ZERO, cam.pos, cam.zoom),
517 None => Point::new(g.size().width * 0.5, g.size().height * 0.5),
518 };
519 g.axes(origin, color, 1.0);
520 }
521 (self.draw)(&mut g);
522 }
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528
529 fn vp(x: f32, y: f32, w: f32, h: f32) -> Rect {
530 Rect::from_xywh(x, y, w, h)
531 }
532
533 #[test]
534 fn fill_helpers_override_canvas_size() {
535 let c = Canvas::new()
536 .width(10.0)
537 .height(20.0)
538 .fill_width()
539 .fill_height();
540 let style = c.layout_style();
541 assert_eq!(style.width, Length::Fill(1.0));
542 assert_eq!(style.height, Length::Fill(1.0));
543 }
544
545 #[test]
546 fn overflow_disables_exact_clip() {
547 use crate::view::View;
548 assert_eq!(Canvas::new().paint_overflow(), 0.0);
549 assert!(Canvas::new().clip_self());
550 let c = Canvas::new().allow_overflow(7.0);
551 assert!((c.paint_overflow() - 7.0).abs() < 1e-6);
552 assert!(!c.clip_self(), "z overflow clip idzie w paint_self");
553 assert!(Canvas::new().allow_overflow(f32::NAN).clip_self());
554 assert!(Canvas::new().allow_overflow(-5.0).clip_self());
555 assert!(!Canvas::new().clip(false).clip_self());
556 }
557
558 #[test]
559 fn camera_roundtrip_local() {
560 let cam = Vec2::new(100.0, 200.0);
561 let world = Point::new(150.0, 260.0);
562 let local = Canvas::world_to_local(world, cam, 2.0);
563 assert_eq!(local, Point::new(100.0, 120.0));
564 assert_eq!(Canvas::local_to_world(local, cam, 2.0), world);
565 assert_eq!(
566 Canvas::local_to_world(local, cam, 0.0),
567 Point::new(cam.x + local.x, cam.y + local.y)
568 );
569 }
570
571 #[test]
572 fn view_bounds_matches_camera() {
573 let size = Size::new(200.0, 100.0);
574 let cam = Vec2::new(0.0, 0.0);
575 let world = Canvas::view_bounds(size, cam, 2.0);
576 assert_eq!(world.size, Size::new(100.0, 50.0));
577 assert!((world.left() - cam.x).abs() < 1e-4);
578 assert!((world.top() - cam.y).abs() < 1e-4);
579 let flat = Canvas::view_bounds(size, cam, 0.0);
580 assert!((flat.size.width - 200.0).abs() < 1e-4);
581 }
582
583 #[test]
584 fn fit_view_centers_content() {
585 let content = vp(0.0, 0.0, 400.0, 200.0);
586 let size = Size::new(800.0, 600.0);
587 let (cam, zoom) = Canvas::fit_view(content, size, 0.0);
588 assert_eq!(zoom, 2.0);
589 let center = Canvas::world_to_local(Point::new(200.0, 100.0), cam, zoom);
590 assert_eq!(center, Point::new(400.0, 300.0));
591 assert_eq!(
592 Canvas::fit_view(content, Size::ZERO, 0.0),
593 (Vec2::new(0.0, 0.0), 1.0)
594 );
595 }
596
597 #[test]
598 fn snap_rounds_to_grid() {
599 assert_eq!(Canvas::snap(13.0, 10.0), 10.0);
600 assert_eq!(Canvas::snap(17.0, 10.0), 20.0);
601 assert_eq!(Canvas::snap(-3.0, 10.0), 0.0);
602 assert_eq!(Canvas::snap(13.0, 0.0), 13.0);
603 assert_eq!(Canvas::snap(13.0, -5.0), 13.0);
604 let p = Canvas::snap_point(Point::new(13.0, 27.0), 10.0);
605 assert_eq!(p, Point::new(10.0, 30.0));
606 }
607
608 #[test]
609 fn lod_bands_from_full_to_coarse() {
610 let breaks = [1.0, 0.55, 0.3];
611 assert_eq!(Canvas::lod_banded(2.0, &breaks), 0);
612 assert_eq!(Canvas::lod_banded(1.0, &breaks), 0);
613 assert_eq!(Canvas::lod_banded(0.7, &breaks), 1);
614 assert_eq!(Canvas::lod_banded(0.55, &breaks), 1);
615 assert_eq!(Canvas::lod_banded(0.4, &breaks), 2);
616 assert_eq!(Canvas::lod_banded(0.1, &breaks), 3);
617 assert_eq!(Canvas::lod_banded(0.1, &[]), 0);
618 assert_eq!(Canvas::lod_banded(0.0, &breaks), 3);
619 assert_eq!(Canvas::lod_banded(-1.0, &breaks), 3);
620 assert_eq!(Canvas::lod_banded(f32::NAN, &breaks), 3);
621 }
622
623 #[test]
624 fn show_detail_hides_subpixel_stuff() {
625 assert!(Canvas::show_detail(1.0, 100.0, 6.0));
626 assert!(Canvas::show_detail(0.06, 100.0, 6.0));
627 assert!(!Canvas::show_detail(0.05, 100.0, 6.0));
628 assert!(!Canvas::show_detail(1.0, 100.0, 0.0));
629 assert!(!Canvas::show_detail(0.0, 100.0, 6.0));
630 assert!(!Canvas::show_detail(f32::NAN, 100.0, 6.0));
631 }
632
633 #[test]
634 fn detail_alpha_ramps_without_popping() {
635 assert_eq!(Canvas::detail_alpha(0.05, 100.0, 6.0, 4.0), 0.0);
636 assert_eq!(Canvas::detail_alpha(0.08, 100.0, 6.0, 4.0), 0.5);
637 assert_eq!(Canvas::detail_alpha(1.0, 100.0, 6.0, 4.0), 1.0);
638 assert_eq!(Canvas::detail_alpha(1.0, 100.0, 6.0, 0.0), 1.0);
639 assert_eq!(Canvas::detail_alpha(0.0, 100.0, 6.0, 4.0), 0.0);
640 }
641
642 #[test]
643 fn default_cursor_is_default() {
644 assert_eq!(
645 Canvas::new().cursor_icon_value(),
646 sequoia_input::CursorIcon::Default
647 );
648 assert_eq!(
649 Canvas::new()
650 .cursor(sequoia_input::CursorIcon::Crosshair)
651 .cursor_icon_value(),
652 sequoia_input::CursorIcon::Crosshair
653 );
654 }
655}