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 on_draw(mut self, draw: impl Fn(&mut Painter) + 'static) -> Self {
182 self.draw = Rc::new(draw);
183 self
184 }
185
186 pub fn on_pointer(
187 mut self,
188 handler: impl Fn(Point, &PointerEvent, &mut EventCtx) + 'static,
189 ) -> Self {
190 self.on_pointer = Some(Rc::new(handler));
191 self
192 }
193
194 pub fn key(mut self, key: Key, modifiers: Modifiers, action: impl Fn() + 'static) -> Self {
195 self.bindings.push(KeyBinding::new(key, modifiers, action));
196 self
197 }
198
199 pub fn on_key(mut self, handler: impl Fn(&KeyEvent) -> bool + 'static) -> Self {
200 self.on_key = Some(Rc::new(handler));
201 self
202 }
203
204 pub fn track(mut self, track: impl Fn() + 'static) -> Self {
205 self.track = Some(Rc::new(track));
206 self
207 }
208
209 pub fn clip(mut self, clip: bool) -> Self {
210 self.clip = clip;
211 self
212 }
213
214 pub fn allow_overflow(mut self, px: f32) -> Self {
215 self.overflow = if px.is_finite() { px.max(0.0) } else { 0.0 };
216 self
217 }
218
219 pub fn overflow_px(self, px: f32) -> Self {
220 self.allow_overflow(px)
221 }
222
223 pub fn paint_overflow(&self) -> f32 {
224 self.overflow
225 }
226
227 pub fn grid(mut self, step: f32) -> Self {
228 self.grid = Some((step, Color::rgba(0.5, 0.5, 0.5, 0.08)));
229 self
230 }
231
232 pub fn grid_colored(mut self, step: f32, color: Color) -> Self {
233 self.grid = Some((step, color));
234 self
235 }
236
237 pub fn axes(mut self) -> Self {
238 self.axes = Some(Color::rgba(0.5, 0.5, 0.5, 0.25));
239 self
240 }
241
242 pub fn axes_colored(mut self, color: Color) -> Self {
243 self.axes = Some(color);
244 self
245 }
246
247 pub fn snap_every(mut self, step: f32) -> Self {
248 self.snap = Some(step);
249 self
250 }
251
252 pub fn snap_step(&self) -> Option<f32> {
253 self.snap
254 }
255
256 pub fn snap_value(&self, value: f32) -> f32 {
257 match self.snap {
258 Some(step) => Self::snap(value, step),
259 None => value,
260 }
261 }
262
263 pub fn camera(mut self, pos: Vec2, zoom: f32) -> Self {
264 self.camera = Some(Camera::new(pos, zoom));
265 self
266 }
267
268 pub fn no_camera(mut self) -> Self {
269 self.camera = None;
270 self
271 }
272
273 pub fn camera_value(&self) -> Option<Camera> {
274 self.camera
275 }
276
277 pub fn background_fill(&self) -> Option<&Fill> {
278 self.background.as_ref()
279 }
280
281 pub fn is_focusable(&self) -> bool {
282 self.focusable
283 }
284
285 pub fn cursor_icon_value(&self) -> CursorIcon {
286 self.cursor
287 }
288
289 pub fn binding_count(&self) -> usize {
290 self.bindings.len()
291 }
292
293 pub fn world_to_local(world: Point, cam: Vec2, zoom: f32) -> Point {
294 let zoom = if zoom.is_finite() && zoom > 0.0 {
295 zoom
296 } else {
297 1.0
298 };
299 Point::new((world.x - cam.x) * zoom, (world.y - cam.y) * zoom)
300 }
301
302 pub fn local_to_world(local: Point, cam: Vec2, zoom: f32) -> Point {
303 let zoom = if zoom.is_finite() && zoom > 0.0 {
304 zoom
305 } else {
306 1.0
307 };
308 Point::new(cam.x + local.x / zoom, cam.y + local.y / zoom)
309 }
310
311 pub fn view_bounds(size: Size, cam: Vec2, zoom: f32) -> Rect {
312 let zoom = if zoom.is_finite() && zoom > 0.0 {
313 zoom
314 } else {
315 1.0
316 };
317 Rect::from_xywh(cam.x, cam.y, size.width / zoom, size.height / zoom)
318 }
319
320 pub fn fit_view(content: Rect, size: Size, padding: f32) -> (Vec2, f32) {
321 if size.width <= 0.0 || size.height <= 0.0 {
322 return (Vec2::new(content.left(), content.top()), 1.0);
323 }
324 let pad = padding.max(0.0);
325 let cw = (content.size.width + pad * 2.0).max(1.0);
326 let ch = (content.size.height + pad * 2.0).max(1.0);
327 let zoom = (size.width / cw).min(size.height / ch).clamp(0.01, 100.0);
328 let cx = content.left() + content.size.width * 0.5;
329 let cy = content.top() + content.size.height * 0.5;
330 let cam = Vec2::new(
331 cx - size.width / (2.0 * zoom),
332 cy - size.height / (2.0 * zoom),
333 );
334 (cam, zoom)
335 }
336
337 pub fn snap(value: f32, step: f32) -> f32 {
338 if !(step > 0.0) || !value.is_finite() {
339 return value;
340 }
341 (value / step).round() * step
342 }
343
344 pub fn snap_point(p: Point, step: f32) -> Point {
345 Point::new(Self::snap(p.x, step), Self::snap(p.y, step))
346 }
347
348 pub fn lod_banded(zoom: f32, breaks: &[f32]) -> usize {
349 if !(zoom > 0.0) {
350 return breaks.len();
351 }
352 for (i, b) in breaks.iter().enumerate() {
353 if zoom >= *b {
354 return i;
355 }
356 }
357 breaks.len()
358 }
359
360 pub fn show_detail(zoom: f32, world: f32, min_px: f32) -> bool {
361 if !(zoom > 0.0) || !(world > 0.0) || !(min_px > 0.0) {
362 return false;
363 }
364 world * zoom >= min_px
365 }
366
367 pub fn detail_alpha(zoom: f32, world: f32, min_px: f32, fade_px: f32) -> f32 {
368 if !(zoom > 0.0) || !(world > 0.0) || !(min_px > 0.0) {
369 return 0.0;
370 }
371 let px = world * zoom;
372 if px <= min_px {
373 return 0.0;
374 }
375 if !(fade_px > 0.0) {
376 return 1.0;
377 }
378 ((px - min_px) / fade_px).clamp(0.0, 1.0)
379 }
380}
381
382impl View for Canvas {
383 fn body(&self) -> impl View + 'static {
384 if let Some(track) = &self.track {
385 track();
386 }
387 crate::views::Unit
388 }
389
390 fn is_leaf(&self) -> bool {
391 true
392 }
393
394 fn focusable(&self) -> bool {
395 self.focusable
396 }
397
398 fn captures_pointer(&self) -> bool {
399 self.capturable
400 }
401
402 fn has_visual_state(&self) -> bool {
403 true
404 }
405
406 fn cursor_icon(&self, _state: sequoia_style::VisualState) -> sequoia_input::CursorIcon {
407 self.cursor
408 }
409
410 fn layout_style(&self) -> LayoutStyle {
411 self.style
412 }
413
414 fn clip_self(&self) -> bool {
415 self.clip && self.overflow <= 0.0
416 }
417
418 fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
419 if ctx.phase != EventPhase::Target {
420 return;
421 }
422 match event {
423 Event::Pointer(ev) => {
424 if self.focusable
425 && let PointerEvent::Down {
426 button: PointerButton::Left,
427 ..
428 } = ev
429 {
430 ctx.request_focus();
431 }
432 if let Some(handler) = &self.on_pointer {
433 let screen = match *ev {
434 PointerEvent::Down { pos, .. }
435 | PointerEvent::Move { pos, .. }
436 | PointerEvent::Up { pos, .. }
437 | PointerEvent::Wheel { pos, .. }
438 | PointerEvent::PenMove { pos, .. }
439 | PointerEvent::PenDown { pos, .. }
440 | PointerEvent::PenUp { pos, .. }
441 | PointerEvent::TouchDown { pos, .. }
442 | PointerEvent::TouchMove { pos, .. }
443 | PointerEvent::TouchUp { pos, .. }
444 | PointerEvent::Pinch { pos, .. } => pos,
445
446 _ => ctx.pos,
447 };
448
449 let mut local =
450 Point::new(screen.x - ctx.rect.left(), screen.y - ctx.rect.top());
451 if let Some(step) = self.snap {
452 local = Self::snap_point(local, step);
453 }
454 handler(local, ev, ctx);
455 }
456 }
457 Event::Key(key) => {
458 for b in &self.bindings {
459 if b.key == key.key && b.modifiers == key.modifiers {
460 b.fire();
461 ctx.handled();
462 return;
463 }
464 }
465 if let Some(handler) = &self.on_key
466 && handler(key)
467 {
468 ctx.handled();
469 }
470 }
471 Event::Ime(_) => {}
472 Event::FilesHovered { .. } | Event::FilesDropped { .. } | Event::FilesCancelled => {}
473
474 _ => {}
475 }
476 }
477
478 fn paint_self(&self, ctx: &mut PaintCtx) {
479 if self.clip && self.overflow > 0.0 && !ctx.rect.is_empty() {
480 ctx.push_clip(ctx.rect.inflate(self.overflow, self.overflow));
481 }
482 let mut g = Painter::new(ctx);
483 if let Some(bg) = &self.background {
484 let rect = g.rect();
485 g.fill(rect, bg.clone());
486 }
487
488 if let Some((step, color)) = self.grid {
489 match self.camera {
490 Some(cam) => {
491 let s = step * cam.zoom;
492 let off = Vec2::new(-cam.pos.x * cam.zoom, -cam.pos.y * cam.zoom);
493 g.grid_offset(s, off, color);
494 }
495 None => {
496 g.grid(step, color);
497 }
498 }
499 }
500 if let Some(color) = self.axes {
501 let origin = match self.camera {
502 Some(cam) => Self::world_to_local(Point::ZERO, cam.pos, cam.zoom),
503 None => Point::new(g.size().width * 0.5, g.size().height * 0.5),
504 };
505 g.axes(origin, color, 1.0);
506 }
507 (self.draw)(&mut g);
508 }
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514
515 fn vp(x: f32, y: f32, w: f32, h: f32) -> Rect {
516 Rect::from_xywh(x, y, w, h)
517 }
518
519 #[test]
520 fn overflow_disables_exact_clip() {
521 use crate::view::View;
522 assert_eq!(Canvas::new().paint_overflow(), 0.0);
523 assert!(Canvas::new().clip_self());
524 let c = Canvas::new().allow_overflow(7.0);
525 assert!((c.paint_overflow() - 7.0).abs() < 1e-6);
526 assert!(!c.clip_self(), "z overflow clip idzie w paint_self");
527 assert!(Canvas::new().allow_overflow(f32::NAN).clip_self());
528 assert!(Canvas::new().allow_overflow(-5.0).clip_self());
529 assert!(!Canvas::new().clip(false).clip_self());
530 }
531
532 #[test]
533 fn camera_roundtrip_local() {
534 let cam = Vec2::new(100.0, 200.0);
535 let world = Point::new(150.0, 260.0);
536 let local = Canvas::world_to_local(world, cam, 2.0);
537 assert_eq!(local, Point::new(100.0, 120.0));
538 assert_eq!(Canvas::local_to_world(local, cam, 2.0), world);
539 assert_eq!(
540 Canvas::local_to_world(local, cam, 0.0),
541 Point::new(cam.x + local.x, cam.y + local.y)
542 );
543 }
544
545 #[test]
546 fn view_bounds_matches_camera() {
547 let size = Size::new(200.0, 100.0);
548 let cam = Vec2::new(0.0, 0.0);
549 let world = Canvas::view_bounds(size, cam, 2.0);
550 assert_eq!(world.size, Size::new(100.0, 50.0));
551 assert!((world.left() - cam.x).abs() < 1e-4);
552 assert!((world.top() - cam.y).abs() < 1e-4);
553 let flat = Canvas::view_bounds(size, cam, 0.0);
554 assert!((flat.size.width - 200.0).abs() < 1e-4);
555 }
556
557 #[test]
558 fn fit_view_centers_content() {
559 let content = vp(0.0, 0.0, 400.0, 200.0);
560 let size = Size::new(800.0, 600.0);
561 let (cam, zoom) = Canvas::fit_view(content, size, 0.0);
562 assert_eq!(zoom, 2.0);
563 let center = Canvas::world_to_local(Point::new(200.0, 100.0), cam, zoom);
564 assert_eq!(center, Point::new(400.0, 300.0));
565 assert_eq!(
566 Canvas::fit_view(content, Size::ZERO, 0.0),
567 (Vec2::new(0.0, 0.0), 1.0)
568 );
569 }
570
571 #[test]
572 fn snap_rounds_to_grid() {
573 assert_eq!(Canvas::snap(13.0, 10.0), 10.0);
574 assert_eq!(Canvas::snap(17.0, 10.0), 20.0);
575 assert_eq!(Canvas::snap(-3.0, 10.0), 0.0);
576 assert_eq!(Canvas::snap(13.0, 0.0), 13.0);
577 assert_eq!(Canvas::snap(13.0, -5.0), 13.0);
578 let p = Canvas::snap_point(Point::new(13.0, 27.0), 10.0);
579 assert_eq!(p, Point::new(10.0, 30.0));
580 }
581
582 #[test]
583 fn lod_bands_from_full_to_coarse() {
584 let breaks = [1.0, 0.55, 0.3];
585 assert_eq!(Canvas::lod_banded(2.0, &breaks), 0);
586 assert_eq!(Canvas::lod_banded(1.0, &breaks), 0);
587 assert_eq!(Canvas::lod_banded(0.7, &breaks), 1);
588 assert_eq!(Canvas::lod_banded(0.55, &breaks), 1);
589 assert_eq!(Canvas::lod_banded(0.4, &breaks), 2);
590 assert_eq!(Canvas::lod_banded(0.1, &breaks), 3);
591 assert_eq!(Canvas::lod_banded(0.1, &[]), 0);
592 assert_eq!(Canvas::lod_banded(0.0, &breaks), 3);
593 assert_eq!(Canvas::lod_banded(-1.0, &breaks), 3);
594 assert_eq!(Canvas::lod_banded(f32::NAN, &breaks), 3);
595 }
596
597 #[test]
598 fn show_detail_hides_subpixel_stuff() {
599 assert!(Canvas::show_detail(1.0, 100.0, 6.0));
600 assert!(Canvas::show_detail(0.06, 100.0, 6.0));
601 assert!(!Canvas::show_detail(0.05, 100.0, 6.0));
602 assert!(!Canvas::show_detail(1.0, 100.0, 0.0));
603 assert!(!Canvas::show_detail(0.0, 100.0, 6.0));
604 assert!(!Canvas::show_detail(f32::NAN, 100.0, 6.0));
605 }
606
607 #[test]
608 fn detail_alpha_ramps_without_popping() {
609 assert_eq!(Canvas::detail_alpha(0.05, 100.0, 6.0, 4.0), 0.0);
610 assert_eq!(Canvas::detail_alpha(0.08, 100.0, 6.0, 4.0), 0.5);
611 assert_eq!(Canvas::detail_alpha(1.0, 100.0, 6.0, 4.0), 1.0);
612 assert_eq!(Canvas::detail_alpha(1.0, 100.0, 6.0, 0.0), 1.0);
613 assert_eq!(Canvas::detail_alpha(0.0, 100.0, 6.0, 4.0), 0.0);
614 }
615
616 #[test]
617 fn default_cursor_is_default() {
618 assert_eq!(
619 Canvas::new().cursor_icon_value(),
620 sequoia_input::CursorIcon::Default
621 );
622 assert_eq!(
623 Canvas::new()
624 .cursor(sequoia_input::CursorIcon::Crosshair)
625 .cursor_icon_value(),
626 sequoia_input::CursorIcon::Crosshair
627 );
628 }
629}