1pub mod layout;
4pub mod node;
5pub mod reconcile;
6
7pub(crate) use self::layout::measure_pan_view;
8pub use self::node::PanViewNode;
9
10use std::sync::Arc;
11
12use crate::callback::Callback;
13use crate::core::element::{Element, ElementKind, Key};
14use crate::core::event::{KeyCode, KeyEvent};
15use crate::style::Length;
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19pub struct PanMetrics {
20 pub content_w: u16,
22 pub content_h: u16,
24 pub viewport_w: u16,
26 pub viewport_h: u16,
28 pub max_x: i32,
30 pub max_y: i32,
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
36pub struct PanEvent {
37 pub x: i32,
39 pub y: i32,
41 pub metrics: PanMetrics,
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
47pub struct PanKeymap(u8);
48
49impl PanKeymap {
50 pub const NONE: Self = Self(0);
52 pub const ARROWS: Self = Self(1 << 0);
54 pub const VIM: Self = Self(1 << 1);
56 pub const DEFAULT: Self = Self(Self::ARROWS.0 | Self::VIM.0);
58
59 pub fn contains(self, other: Self) -> bool {
61 (self.0 & other.0) == other.0
62 }
63}
64
65impl std::ops::BitOr for PanKeymap {
66 type Output = Self;
67
68 fn bitor(self, rhs: Self) -> Self {
69 Self(self.0 | rhs.0)
70 }
71}
72
73impl std::ops::BitOrAssign for PanKeymap {
74 fn bitor_assign(&mut self, rhs: Self) {
75 self.0 |= rhs.0;
76 }
77}
78
79impl std::ops::BitAnd for PanKeymap {
80 type Output = Self;
81
82 fn bitand(self, rhs: Self) -> Self {
83 Self(self.0 & rhs.0)
84 }
85}
86
87impl std::ops::BitAndAssign for PanKeymap {
88 fn bitand_assign(&mut self, rhs: Self) {
89 self.0 &= rhs.0;
90 }
91}
92
93impl std::ops::Not for PanKeymap {
94 type Output = Self;
95
96 fn not(self) -> Self {
97 Self(!self.0)
98 }
99}
100
101impl Default for PanKeymap {
102 fn default() -> Self {
103 Self::DEFAULT
104 }
105}
106
107#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
108pub(crate) enum PanAction {
109 Delta(i16, i16),
110}
111
112fn signed_step(step: u16) -> i16 {
113 step.min(i16::MAX as u16) as i16
114}
115
116pub(crate) fn pan_action_from_key(
117 key: &KeyEvent,
118 keymap: PanKeymap,
119 key_step: (u16, u16),
120) -> Option<PanAction> {
121 let x = signed_step(key_step.0);
122 let y = signed_step(key_step.1);
123 match key.code {
124 KeyCode::Left if keymap.contains(PanKeymap::ARROWS) => Some(PanAction::Delta(-x, 0)),
125 KeyCode::Right if keymap.contains(PanKeymap::ARROWS) => Some(PanAction::Delta(x, 0)),
126 KeyCode::Up if keymap.contains(PanKeymap::ARROWS) => Some(PanAction::Delta(0, -y)),
127 KeyCode::Down if keymap.contains(PanKeymap::ARROWS) => Some(PanAction::Delta(0, y)),
128 KeyCode::Char('h') if keymap.contains(PanKeymap::VIM) => Some(PanAction::Delta(-x, 0)),
129 KeyCode::Char('l') if keymap.contains(PanKeymap::VIM) => Some(PanAction::Delta(x, 0)),
130 KeyCode::Char('k') if keymap.contains(PanKeymap::VIM) => Some(PanAction::Delta(0, -y)),
131 KeyCode::Char('j') if keymap.contains(PanKeymap::VIM) => Some(PanAction::Delta(0, y)),
132 _ => None,
133 }
134}
135
136pub(crate) fn pan_metrics(
137 content_w: u16,
138 content_h: u16,
139 viewport_w: u16,
140 viewport_h: u16,
141) -> PanMetrics {
142 PanMetrics {
143 content_w,
144 content_h,
145 viewport_w,
146 viewport_h,
147 max_x: i32::from(content_w.saturating_sub(viewport_w)),
148 max_y: i32::from(content_h.saturating_sub(viewport_h)),
149 }
150}
151
152pub(crate) fn clamp_pan_offset((x, y): (i32, i32), metrics: PanMetrics, clamp: bool) -> (i32, i32) {
153 if clamp {
154 (x.clamp(0, metrics.max_x), y.clamp(0, metrics.max_y))
155 } else {
156 (x, y)
157 }
158}
159
160fn free_axis_bounds(content: u16, viewport: u16, margin: u16) -> (i32, i32) {
161 let margin = margin.min(content.max(1)).min(viewport.max(1));
162 (
163 -(i32::from(viewport) - i32::from(margin)),
164 i32::from(content) - i32::from(margin),
165 )
166}
167
168pub(crate) fn bound_pan_offset(
169 (x, y): (i32, i32),
170 metrics: PanMetrics,
171 clamp: bool,
172 free_pan_margin: Option<(u16, u16)>,
173) -> (i32, i32) {
174 if clamp {
175 return clamp_pan_offset((x, y), metrics, true);
176 }
177
178 let Some((margin_x, margin_y)) = free_pan_margin else {
179 return (x, y);
180 };
181
182 let (min_x, max_x) = free_axis_bounds(metrics.content_w, metrics.viewport_w, margin_x);
183 let (min_y, max_y) = free_axis_bounds(metrics.content_h, metrics.viewport_h, margin_y);
184 (x.clamp(min_x, max_x), y.clamp(min_y, max_y))
185}
186
187pub(crate) fn apply_pan_delta(
188 (x, y): (i32, i32),
189 dx: i16,
190 dy: i16,
191 metrics: PanMetrics,
192 clamp: bool,
193 free_pan_margin: Option<(u16, u16)>,
194) -> (i32, i32) {
195 let next_x = x.saturating_add(i32::from(dx));
196 let next_y = y.saturating_add(i32::from(dy));
197 bound_pan_offset((next_x, next_y), metrics, clamp, free_pan_margin)
198}
199
200pub(crate) fn apply_pan_action(
201 offset: (i32, i32),
202 action: PanAction,
203 metrics: PanMetrics,
204 clamp: bool,
205 free_pan_margin: Option<(u16, u16)>,
206) -> (i32, i32) {
207 match action {
208 PanAction::Delta(dx, dy) => {
209 apply_pan_delta(offset, dx, dy, metrics, clamp, free_pan_margin)
210 }
211 }
212}
213
214#[derive(Clone)]
216pub struct PanView {
217 pub(crate) width: Length,
218 pub(crate) height: Length,
219 pub(crate) offset: Option<(i32, i32)>,
220 pub(crate) on_pan: Option<Callback<PanEvent>>,
221 pub(crate) clamp: bool,
222 pub(crate) center_content: bool,
223 pub(crate) free_pan_margin: Option<(u16, u16)>,
224 pub(crate) drag_to_pan: bool,
225 pub(crate) wheel_to_pan: bool,
226 pub(crate) keymap: PanKeymap,
227 pub(crate) key_step: (u16, u16),
228 pub(crate) focusable: bool,
229 pub(crate) tab_stop: bool,
230 pub(crate) on_focus: Option<Callback<()>>,
231 pub(crate) on_blur: Option<Callback<()>>,
232 pub(crate) pan_state_key: Option<Key>,
233 pub(crate) child: Option<Box<Element>>,
234}
235
236impl Default for PanView {
237 fn default() -> Self {
238 Self {
239 width: Length::Flex(1),
240 height: Length::Flex(1),
241 offset: None,
242 on_pan: None,
243 clamp: true,
244 center_content: false,
245 free_pan_margin: None,
246 drag_to_pan: true,
247 wheel_to_pan: true,
248 keymap: PanKeymap::default(),
249 key_step: (4, 2),
250 focusable: false,
251 tab_stop: true,
252 on_focus: None,
253 on_blur: None,
254 pan_state_key: None,
255 child: None,
256 }
257 }
258}
259
260impl PanView {
261 pub fn new() -> Self {
263 Self::default()
264 }
265
266 pub fn child(mut self, child: impl Into<Element>) -> Self {
268 self.child = Some(Box::new(child.into()));
269 self
270 }
271
272 pub fn width(mut self, width: Length) -> Self {
274 self.width = width;
275 self
276 }
277
278 pub fn height(mut self, height: Length) -> Self {
280 self.height = height;
281 self
282 }
283
284 pub fn offset(mut self, offset: (i32, i32)) -> Self {
286 self.offset = Some(offset);
287 self
288 }
289
290 pub fn on_pan(mut self, cb: Callback<PanEvent>) -> Self {
292 self.on_pan = Some(cb);
293 self
294 }
295
296 pub fn clamp(mut self, clamp: bool) -> Self {
298 self.clamp = clamp;
299 self
300 }
301
302 pub fn center_content(mut self, center: bool) -> Self {
304 self.center_content = center;
305 self
306 }
307
308 pub fn free_pan_margin(mut self, margin: u16) -> Self {
313 self.free_pan_margin = Some((margin, margin));
314 self
315 }
316
317 pub fn free_pan_margins(mut self, margins: (u16, u16)) -> Self {
319 self.free_pan_margin = Some(margins);
320 self
321 }
322
323 pub fn drag_to_pan(mut self, enabled: bool) -> Self {
325 self.drag_to_pan = enabled;
326 self
327 }
328
329 pub fn wheel_to_pan(mut self, enabled: bool) -> Self {
335 self.wheel_to_pan = enabled;
336 self
337 }
338
339 pub fn keymap(mut self, keymap: PanKeymap) -> Self {
341 self.keymap = keymap;
342 if keymap != PanKeymap::NONE {
343 self.focusable = true;
344 }
345 self
346 }
347
348 pub fn pan_keys(self, keymap: PanKeymap) -> Self {
350 self.keymap(keymap)
351 }
352
353 pub fn key_step(mut self, step: (u16, u16)) -> Self {
357 self.key_step = (step.0.max(1), step.1.max(1));
358 self
359 }
360
361 pub fn focusable(mut self, focusable: bool) -> Self {
363 self.focusable = focusable;
364 self
365 }
366
367 pub fn tab_stop(mut self, tab_stop: bool) -> Self {
369 self.tab_stop = tab_stop;
370 self
371 }
372
373 pub fn on_focus(mut self, cb: Callback<()>) -> Self {
375 self.on_focus = Some(cb);
376 self
377 }
378
379 pub fn on_blur(mut self, cb: Callback<()>) -> Self {
381 self.on_blur = Some(cb);
382 self
383 }
384
385 pub fn pan_state_key(mut self, key: impl Into<Key>) -> Self {
387 self.pan_state_key = Some(key.into());
388 self
389 }
390
391 pub fn state_key(self, key: impl Into<Arc<str>>) -> Self {
393 self.pan_state_key(Key::from(key.into()))
394 }
395}
396
397impl From<PanView> for Element {
398 fn from(value: PanView) -> Self {
399 Element::new(ElementKind::PanView(value))
400 }
401}
402
403impl crate::layout::hash::LayoutHash for PanView {
404 fn layout_hash(
405 &self,
406 hasher: &mut impl std::hash::Hasher,
407 recurse: &dyn Fn(&Element) -> Option<u64>,
408 ) -> Option<()> {
409 use std::hash::Hash;
410
411 self.width.hash(hasher);
412 self.height.hash(hasher);
413 self.clamp.hash(hasher);
414 self.center_content.hash(hasher);
415 self.free_pan_margin.hash(hasher);
416 self.drag_to_pan.hash(hasher);
417 self.wheel_to_pan.hash(hasher);
418 self.keymap.hash(hasher);
419 self.key_step.hash(hasher);
420 self.focusable.hash(hasher);
421 if let Some(child) = &self.child {
422 recurse(child)?.hash(hasher);
423 }
424 Some(())
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431 use crate::core::node::{NodeKind, NodeTree};
432 use crate::layout::LayoutEngine;
433 use crate::style::Rect;
434 use crate::widgets::Text;
435
436 fn wide_tall_child() -> Element {
437 Text::new("0123456789\n0123456789\n0123456789").into()
438 }
439
440 fn small_child() -> Element {
441 Text::new("tiny").into()
442 }
443
444 #[test]
445 fn pan_view_clamps_offsets_to_content_bounds() {
446 let metrics = pan_metrics(20, 10, 8, 4);
447
448 assert_eq!(clamp_pan_offset((99, 99), metrics, true), (12, 6));
449 assert_eq!(clamp_pan_offset((99, 99), metrics, false), (99, 99));
450 assert_eq!(clamp_pan_offset((-4, -2), metrics, false), (-4, -2));
451 }
452
453 #[test]
454 fn pan_view_drag_direction_decreases_offset_when_dragging_right_down() {
455 let metrics = pan_metrics(20, 10, 8, 4);
456
457 assert_eq!(apply_pan_delta((5, 3), -2, -1, metrics, true, None), (3, 2));
458 assert_eq!(apply_pan_delta((5, 3), 2, 1, metrics, true, None), (7, 4));
459 assert_eq!(
460 apply_pan_delta((0, 0), -2, -1, metrics, false, None),
461 (-2, -1)
462 );
463 }
464
465 #[test]
466 fn pan_view_free_pan_margin_bounds_unclamped_offsets() {
467 let metrics = pan_metrics(20, 10, 8, 4);
468
469 assert_eq!(
470 bound_pan_offset((-99, -99), metrics, false, Some((1, 1))),
471 (-7, -3)
472 );
473 assert_eq!(
474 bound_pan_offset((99, 99), metrics, false, Some((1, 1))),
475 (19, 9)
476 );
477 }
478
479 #[test]
480 fn pan_view_keymap_matches_arrows_and_vim() {
481 let key = |code| KeyEvent {
482 code,
483 mods: crate::core::event::KeyMods::default(),
484 };
485
486 assert_eq!(
487 pan_action_from_key(&key(KeyCode::Right), PanKeymap::ARROWS, (4, 2)),
488 Some(PanAction::Delta(4, 0))
489 );
490 assert_eq!(
491 pan_action_from_key(&key(KeyCode::Char('h')), PanKeymap::VIM, (4, 2)),
492 Some(PanAction::Delta(-4, 0))
493 );
494 assert_eq!(
495 pan_action_from_key(&key(KeyCode::Char('h')), PanKeymap::ARROWS, (4, 2)),
496 None
497 );
498 assert_eq!(
499 pan_action_from_key(&key(KeyCode::Char('u')), PanKeymap::VIM, (4, 2)),
500 None
501 );
502 }
503
504 #[test]
505 fn pan_view_reconcile_clamps_and_offsets_child_rect() {
506 let root: Element = PanView::new()
507 .width(Length::Px(5))
508 .height(Length::Px(2))
509 .offset((99, 99))
510 .child(wide_tall_child())
511 .into();
512 let mut tree = NodeTree::new();
513 LayoutEngine::reconcile_with_focus(
514 &mut tree,
515 &root,
516 Rect {
517 x: 0,
518 y: 0,
519 w: 5,
520 h: 2,
521 },
522 None,
523 );
524
525 let node = tree.node(tree.root);
526 let NodeKind::PanView(pan) = &node.kind else {
527 panic!("expected PanView root");
528 };
529 assert_eq!((pan.content_w, pan.content_h), (10, 3));
530 assert_eq!((pan.viewport_w, pan.viewport_h), (5, 2));
531 assert_eq!((pan.offset_x, pan.offset_y), (5, 1));
532
533 let child = tree.node(node.children[0]);
534 assert_eq!(child.rect.x, -5);
535 assert_eq!(child.rect.y, -1);
536 assert_eq!(child.rect.w, 10);
537 assert_eq!(child.rect.h, 3);
538 }
539
540 #[test]
541 fn pan_view_keyboard_updates_uncontrolled_offset_and_persists_key() {
542 use std::cell::RefCell;
543 use std::rc::Rc;
544
545 let events = Rc::new(RefCell::new(Vec::new()));
546 let events_cb = events.clone();
547 let root: Element = PanView::new()
548 .width(Length::Px(5))
549 .height(Length::Px(2))
550 .pan_state_key("pan-test")
551 .on_pan(Callback::new(move |event| {
552 events_cb.borrow_mut().push(event);
553 }))
554 .child(wide_tall_child())
555 .into();
556 let mut tree = NodeTree::new();
557 let bounds = Rect {
558 x: 0,
559 y: 0,
560 w: 5,
561 h: 2,
562 };
563 LayoutEngine::reconcile_with_focus(&mut tree, &root, bounds, None);
564
565 let root_id = tree.root;
566 let handled = crate::app::input::handlers::pan_view::handle_key(
567 &mut tree,
568 root_id,
569 &KeyEvent {
570 code: KeyCode::Right,
571 mods: crate::core::event::KeyMods::default(),
572 },
573 );
574 assert!(handled);
575 let NodeKind::PanView(pan) = &tree.node(tree.root).kind else {
576 panic!("expected PanView root");
577 };
578 assert_eq!((pan.offset_x, pan.offset_y), (4, 0));
579 assert_eq!(events.borrow()[0].x, 4);
580
581 LayoutEngine::reconcile_with_focus(&mut tree, &root, bounds, None);
582 let NodeKind::PanView(pan) = &tree.node(tree.root).kind else {
583 panic!("expected PanView root");
584 };
585 assert_eq!((pan.offset_x, pan.offset_y), (4, 0));
586 }
587
588 #[test]
589 fn pan_view_can_center_smaller_content_with_negative_offset() {
590 let root: Element = PanView::new()
591 .width(Length::Px(20))
592 .height(Length::Px(10))
593 .clamp(false)
594 .center_content(true)
595 .child(small_child())
596 .into();
597 let mut tree = NodeTree::new();
598 LayoutEngine::reconcile_with_focus(
599 &mut tree,
600 &root,
601 Rect {
602 x: 0,
603 y: 0,
604 w: 20,
605 h: 10,
606 },
607 None,
608 );
609
610 let node = tree.node(tree.root);
611 let NodeKind::PanView(pan) = &node.kind else {
612 panic!("expected PanView root");
613 };
614 assert_eq!((pan.offset_x, pan.offset_y), (-8, -4));
615 let child = tree.node(node.children[0]);
616 assert_eq!(child.rect.x, 8);
617 assert_eq!(child.rect.y, 4);
618 }
619}